Add Spring Security Kerberos
Move the Spring Security Kerberos Extension into Spring Security Closes gh-17879
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -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[]
|
|
@ -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() {}
|
||||
}
|
|
@ -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[]
|
|
@ -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[]
|
||||
|
||||
}
|
|
@ -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[]
|
||||
|
||||
}
|
|
@ -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[]
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
= Spring Security Kerberos
|
||||
|
||||
Spring Security Kerberos adds the ability to work with Kerberos and Spring applications.
|
|
@ -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}.
|
|
@ -0,0 +1,225 @@
|
|||
[[springsecuritykerberossamples]]
|
||||
= Spring Security Kerberos Samples
|
||||
:figures: servlet/authentication/kerberos
|
||||
|
||||
This part of the reference documentation is introducing samples
|
||||
projects. Samples can be compiled manually by building main
|
||||
distribution from
|
||||
https://github.com/spring-projects/spring-security-kerberos.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
If you run sample as is it will not work until a correct configuration
|
||||
is applied. See notes below for specific samples.
|
||||
====
|
||||
|
||||
<<samples-sec-server-win-auth>> sample for Windows environment
|
||||
|
||||
<<samples-sec-server-client-auth>> sample using server side authenticator
|
||||
|
||||
<<samples-sec-server-spnego-form-auth>> sample using ticket validation
|
||||
with spnego and form
|
||||
|
||||
<<samples-sec-client-rest-template>> sample for KerberosRestTemplate
|
||||
|
||||
[[samples-sec-server-win-auth]]
|
||||
== Security Server Windows Auth Sample
|
||||
Goals of this sample:
|
||||
|
||||
- In windows environment, User will be able to logon to application
|
||||
with Windows Active directory Credential which has been entered
|
||||
during log on to windows. There should not be any ask for
|
||||
userid/password credentials.
|
||||
- In non-windows environment, User will be presented with a screen
|
||||
to provide Active directory credentials.
|
||||
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
server:
|
||||
port: 8080
|
||||
app:
|
||||
ad-domain: EXAMPLE.ORG
|
||||
ad-server: ldap://WIN-EKBO0EQ7TS7.example.org/
|
||||
service-principal: HTTP/neo.example.org@EXAMPLE.ORG
|
||||
keytab-location: /tmp/tomcat.keytab
|
||||
ldap-search-base: dc=example,dc=org
|
||||
ldap-search-filter: "(| (userPrincipalName={0}) (sAMAccountName={0}))"
|
||||
----
|
||||
In above you can see the default configuration for this sample. You
|
||||
can override these settings using a normal Spring Boot tricks like
|
||||
using command-line options or custom `application.yml` file.
|
||||
|
||||
Run a server.
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
$ java -jar sec-server-win-auth-{spring-security-version}.jar
|
||||
----
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
You may need to use custom kerberos config with Linux either by using
|
||||
`-Djava.security.krb5.conf=/path/to/krb5.ini` or
|
||||
`GlobalSunJaasKerberosConfig` bean.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
See xref:servlet/authentication/kerberos/appendix.adoc#setupwinkerberos[Setup Windows Domain Controller]
|
||||
for more instructions how to work with windows kerberos environment.
|
||||
====
|
||||
|
||||
Login to `Windows 8.1` using domain credentials and access sample
|
||||
|
||||
image::{figures}/ie1.png[]
|
||||
image::{figures}/ie2.png[]
|
||||
|
||||
Access sample application from a non windows vm and use domain
|
||||
credentials manually.
|
||||
|
||||
image::{figures}/ff1.png[]
|
||||
image::{figures}/ff2.png[]
|
||||
image::{figures}/ff3.png[]
|
||||
|
||||
|
||||
[[samples-sec-server-client-auth]]
|
||||
== Security Server Side Auth Sample
|
||||
This sample demonstrates how server is able to authenticate user
|
||||
against kerberos environment using his credentials passed in via a
|
||||
form login.
|
||||
|
||||
Run a server.
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
$ java -jar sec-server-client-auth-{spring-security-version}.jar
|
||||
----
|
||||
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
server:
|
||||
port: 8080
|
||||
----
|
||||
|
||||
[[samples-sec-server-spnego-form-auth]]
|
||||
== Security Server Spnego and Form Auth Sample
|
||||
This sample demonstrates how a server can be configured to accept a
|
||||
Spnego based negotiation from a browser while still being able to fall
|
||||
back to a form based authentication.
|
||||
|
||||
Using a `user1` principal xref:servlet/authentication/kerberos/appendix.adoc#setupmitkerberos[Setup MIT Kerberos],
|
||||
do a kerberos login manually using credentials.
|
||||
[source,text]
|
||||
----
|
||||
$ kinit user1
|
||||
Password for user1@EXAMPLE.ORG:
|
||||
|
||||
$ klist
|
||||
Ticket cache: FILE:/tmp/krb5cc_1000
|
||||
Default principal: user1@EXAMPLE.ORG
|
||||
|
||||
Valid starting Expires Service principal
|
||||
10/03/15 17:18:45 11/03/15 03:18:45 krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
|
||||
renew until 11/03/15 17:18:40
|
||||
----
|
||||
|
||||
or using a keytab file.
|
||||
|
||||
[source,text]
|
||||
----
|
||||
$ kinit -kt user2.keytab user1
|
||||
|
||||
$ klist
|
||||
Ticket cache: FILE:/tmp/krb5cc_1000
|
||||
Default principal: user2@EXAMPLE.ORG
|
||||
|
||||
Valid starting Expires Service principal
|
||||
10/03/15 17:25:03 11/03/15 03:25:03 krbtgt/EXAMPLE.ORG@EXAMPLE.ORG
|
||||
renew until 11/03/15 17:25:03
|
||||
----
|
||||
|
||||
Run a server.
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
$ java -jar sec-server-spnego-form-auth-{spring-security-version}.jar
|
||||
----
|
||||
|
||||
Now you should be able to open your browser and let it do Spnego
|
||||
authentication with existing ticket.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
See xref:servlet/authentication/kerberos/appendix.adoc#browserspnegoconfig[Configure Browsers for Spnego Negotiation]
|
||||
for more instructions for configuring browsers to use Spnego.
|
||||
====
|
||||
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
server:
|
||||
port: 8080
|
||||
app:
|
||||
service-principal: HTTP/neo.example.org@EXAMPLE.ORG
|
||||
keytab-location: /tmp/tomcat.keytab
|
||||
----
|
||||
|
||||
[[samples-sec-client-rest-template]]
|
||||
== Security Client KerberosRestTemplate Sample
|
||||
This is a sample using a Spring RestTemplate to access Kerberos
|
||||
protected resource. You can use this together with
|
||||
<<samples-sec-server-spnego-form-auth>>.
|
||||
|
||||
Default application is configured as shown below.
|
||||
[source,yaml,indent=0]
|
||||
----
|
||||
app:
|
||||
user-principal: user2@EXAMPLE.ORG
|
||||
keytab-location: /tmp/user2.keytab
|
||||
access-url: http://neo.example.org:8080/hello
|
||||
----
|
||||
|
||||
|
||||
Using a `user1` principal xref:servlet/authentication/kerberos/appendix.adoc#setupmitkerberos[Setup MIT Kerberos],
|
||||
do a kerberos login manually using credentials.
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
$ java -jar sec-client-rest-template-{spring-security-version}.jar --app.user-principal --app.keytab-location
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
In above we simply set `app.user-principal` and `app.keytab-location`
|
||||
to empty values which disables a use of keytab file.
|
||||
====
|
||||
|
||||
If operation is succesfull you should see below output with `user1@EXAMPLE.ORG`.
|
||||
[source,text]
|
||||
----
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
|
||||
<head>
|
||||
<title>Spring Security Kerberos Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello user1@EXAMPLE.ORG!</h1>
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
|
||||
Or use a `user2` with a keytab file.
|
||||
[source,text,subs="attributes"]
|
||||
----
|
||||
$ java -jar sec-client-rest-template-{spring-security-version}.jar
|
||||
----
|
||||
|
||||
If operation is succesfull you should see below output with `user2@EXAMPLE.ORG`.
|
||||
[source,text]
|
||||
----
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
|
||||
<head>
|
||||
<title>Spring Security Kerberos Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello user2@EXAMPLE.ORG!</h1>
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
[[springsecuritykerberos]]
|
||||
= Spring and Spring Security Kerberos
|
||||
:figures: servlet/authentication/kerberos
|
||||
|
||||
This part of the reference documentation explains the core functionality
|
||||
that Spring Security Kerberos provides to any Spring based application.
|
||||
|
||||
<<ssk-authprovider>> describes the authentication provider support.
|
||||
|
||||
<<ssk-spnego>> describes the spnego negotiate support.
|
||||
|
||||
<<ssk-resttemplate>> describes the RestTemplate support.
|
||||
|
||||
|
||||
[[ssk-authprovider]]
|
||||
== Authentication Provider
|
||||
|
||||
Provider configuration using JavaConfig.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$kerberos/AuthProviderConfig.java[tags=snippetA]
|
||||
----
|
||||
|
||||
[[ssk-spnego]]
|
||||
== Spnego Negotiate
|
||||
|
||||
Spnego configuration using JavaConfig.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$kerberos/SpnegoConfig.java[tags=snippetA]
|
||||
----
|
||||
|
||||
[[ssk-resttemplate]]
|
||||
== Using KerberosRestTemplate
|
||||
|
||||
If there is a need to access Kerberos protected web resources
|
||||
programmatically we have `KerberosRestTemplate` which extends
|
||||
`RestTemplate` and does necessary login actions prior to delegating to
|
||||
actual RestTemplate methods. You basically have few options to
|
||||
configure this template.
|
||||
|
||||
- Leave keyTabLocation and userPrincipal empty if you want to
|
||||
use cached ticket.
|
||||
- Use keyTabLocation and userPrincipal if you want to use
|
||||
keytab file.
|
||||
- Use loginOptions if you want to customise Krb5LoginModule options.
|
||||
- Use a customised httpClient.
|
||||
|
||||
With ticket cache.
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$kerberos/KerberosRestTemplateConfig.java[tags=snippetA]
|
||||
----
|
||||
|
||||
With keytab file.
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$kerberos/KerberosRestTemplateConfig.java[tags=snippetB]
|
||||
----
|
||||
|
||||
[[ssk-kerberosldap]]
|
||||
== Authentication with LDAP Services
|
||||
|
||||
With most of your samples we're using `DummyUserDetailsService`
|
||||
because there is not necessarily need to query a real user details
|
||||
once kerberos authentication is successful and we can use kerberos
|
||||
principal info to create that dummy user. However there is a way to
|
||||
access kerberized LDAP services in a say way and query user details
|
||||
from there.
|
||||
|
||||
`KerberosLdapContextSource` can be used to bind into LDAP via kerberos
|
||||
which is at least proven to work well with Windows AD services.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$kerberos/KerberosLdapContextSourceConfig.java[tags=snippetA]
|
||||
----
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Sample xref:servlet/authentication/kerberos/samples.adoc#samples-sec-server-win-auth[Security Server Windows Auth Sample]
|
||||
is currently configured to query user details from AD if authentication happen via kerberos.
|
||||
====
|
|
@ -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`
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.NameCallback;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
|
||||
import org.apache.hc.client5.http.auth.AuthSchemeFactory;
|
||||
import org.apache.hc.client5.http.auth.AuthScope;
|
||||
import org.apache.hc.client5.http.auth.Credentials;
|
||||
import org.apache.hc.client5.http.auth.KerberosConfig;
|
||||
import org.apache.hc.client5.http.auth.StandardAuthScheme;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
|
||||
import org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.core5.http.config.Lookup;
|
||||
import org.apache.hc.core5.http.config.RegistryBuilder;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RequestCallback;
|
||||
import org.springframework.web.client.ResponseExtractor;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* {@code RestTemplate} that is able to make kerberos SPNEGO authenticated REST requests.
|
||||
* Under a hood this {@code KerberosRestTemplate} is using {@link HttpClient} to support
|
||||
* Kerberos.
|
||||
*
|
||||
* <p>
|
||||
* Generally this template can be configured in few different ways.
|
||||
* <ul>
|
||||
* <li>Leave keyTabLocation and userPrincipal empty if you want to use cached ticket</li>
|
||||
* <li>Use keyTabLocation and userPrincipal if you want to use keytab file</li>
|
||||
* <li>Use userPrincipal and password if you want to use user/password</li>
|
||||
* <li>Use loginOptions if you want to customise Krb5LoginModule options</li>
|
||||
* <li>Use a customised httpClient</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
public class KerberosRestTemplate extends RestTemplate {
|
||||
|
||||
private static final Credentials credentials = new NullCredentials();
|
||||
|
||||
private final String keyTabLocation;
|
||||
|
||||
private final String userPrincipal;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final Map<String, Object> loginOptions;
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
*/
|
||||
public KerberosRestTemplate() {
|
||||
this(null, null, null, null, buildHttpClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param httpClient the http client
|
||||
*/
|
||||
public KerberosRestTemplate(HttpClient httpClient) {
|
||||
this(null, null, null, null, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param keyTabLocation the key tab location
|
||||
* @param userPrincipal the user principal
|
||||
*/
|
||||
public KerberosRestTemplate(String keyTabLocation, String userPrincipal) {
|
||||
this(keyTabLocation, userPrincipal, buildHttpClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param keyTabLocation the key tab location
|
||||
* @param userPrincipal the user principal
|
||||
* @param httpClient the http client
|
||||
*/
|
||||
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, HttpClient httpClient) {
|
||||
this(keyTabLocation, userPrincipal, null, null, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param loginOptions the login options
|
||||
*/
|
||||
public KerberosRestTemplate(Map<String, Object> loginOptions) {
|
||||
this(null, null, null, loginOptions, buildHttpClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param loginOptions the login options
|
||||
* @param httpClient the http client
|
||||
*/
|
||||
public KerberosRestTemplate(Map<String, Object> loginOptions, HttpClient httpClient) {
|
||||
this(null, null, null, loginOptions, httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param keyTabLocation the key tab location
|
||||
* @param userPrincipal the user principal
|
||||
* @param loginOptions the login options
|
||||
*/
|
||||
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, Map<String, Object> loginOptions) {
|
||||
this(keyTabLocation, userPrincipal, null, loginOptions, buildHttpClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param keyTabLocation the key tab location
|
||||
* @param userPrincipal the user principal
|
||||
* @param password the password
|
||||
* @param loginOptions the login options
|
||||
*/
|
||||
public KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
|
||||
Map<String, Object> loginOptions) {
|
||||
this(keyTabLocation, userPrincipal, password, loginOptions, buildHttpClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos rest template.
|
||||
* @param keyTabLocation the key tab location
|
||||
* @param userPrincipal the user principal
|
||||
* @param password the password
|
||||
* @param loginOptions the login options
|
||||
* @param httpClient the http client
|
||||
*/
|
||||
private KerberosRestTemplate(String keyTabLocation, String userPrincipal, String password,
|
||||
Map<String, Object> loginOptions, HttpClient httpClient) {
|
||||
super(new HttpComponentsClientHttpRequestFactory(httpClient));
|
||||
this.keyTabLocation = keyTabLocation;
|
||||
this.userPrincipal = userPrincipal;
|
||||
this.password = password;
|
||||
this.loginOptions = loginOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the default instance of {@link HttpClient} having kerberos support.
|
||||
* @return the http client with spneno auth scheme
|
||||
*/
|
||||
private static HttpClient buildHttpClient() {
|
||||
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||
|
||||
Lookup<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
|
||||
.register(StandardAuthScheme.SPNEGO,
|
||||
new SPNegoSchemeFactory(KerberosConfig.custom()
|
||||
.setStripPort(KerberosConfig.Option.ENABLE)
|
||||
.setUseCanonicalHostname(KerberosConfig.Option.DISABLE)
|
||||
.build(), SystemDefaultDnsResolver.INSTANCE))
|
||||
.build();
|
||||
|
||||
builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);
|
||||
RequestConfig negotiate = RequestConfig.copy(RequestConfig.DEFAULT)
|
||||
.setTargetPreferredAuthSchemes(Set.of(StandardAuthScheme.SPNEGO, StandardAuthScheme.KERBEROS))
|
||||
.build();
|
||||
builder.setDefaultRequestConfig(negotiate);
|
||||
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(new AuthScope(null, -1), credentials);
|
||||
builder.setDefaultCredentialsProvider(credentialsProvider);
|
||||
CloseableHttpClient httpClient = builder.build();
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the {@link LoginContext} with credentials and options for authentication
|
||||
* against kerberos.
|
||||
* @return the login context
|
||||
*/
|
||||
private LoginContext buildLoginContext() throws LoginException {
|
||||
ClientLoginConfig loginConfig = new ClientLoginConfig(this.keyTabLocation, this.userPrincipal, this.password,
|
||||
this.loginOptions);
|
||||
Set<Principal> princ = new HashSet<Principal>(1);
|
||||
if (this.userPrincipal != null) {
|
||||
princ.add(new KerberosPrincipal(this.userPrincipal));
|
||||
}
|
||||
Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
|
||||
CallbackHandler callbackHandler = new CallbackHandlerImpl(this.userPrincipal, this.password);
|
||||
LoginContext lc = new LoginContext("", sub, callbackHandler, loginConfig);
|
||||
return lc;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final <T> T doExecute(final URI url, final String uriTemplate, final HttpMethod method,
|
||||
final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
|
||||
throws RestClientException {
|
||||
|
||||
try {
|
||||
LoginContext lc = buildLoginContext();
|
||||
lc.login();
|
||||
Subject serviceSubject = lc.getSubject();
|
||||
return Subject.doAs(serviceSubject, new PrivilegedAction<T>() {
|
||||
|
||||
@Override
|
||||
public T run() {
|
||||
return KerberosRestTemplate.this.doExecuteSubject(url, uriTemplate, method, requestCallback,
|
||||
responseExtractor);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RestClientException("Error running rest call", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T doExecuteSubject(URI url, String uriTemplate, HttpMethod method, RequestCallback requestCallback,
|
||||
ResponseExtractor<T> responseExtractor) throws RestClientException {
|
||||
return super.doExecute(url, uriTemplate, method, requestCallback, responseExtractor);
|
||||
}
|
||||
|
||||
private static final class ClientLoginConfig extends Configuration {
|
||||
|
||||
private final String keyTabLocation;
|
||||
|
||||
private final String userPrincipal;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final Map<String, Object> loginOptions;
|
||||
|
||||
private ClientLoginConfig(String keyTabLocation, String userPrincipal, String password,
|
||||
Map<String, Object> loginOptions) {
|
||||
super();
|
||||
this.keyTabLocation = keyTabLocation;
|
||||
this.userPrincipal = userPrincipal;
|
||||
this.password = password;
|
||||
this.loginOptions = loginOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
|
||||
Map<String, Object> options = new HashMap<String, Object>();
|
||||
|
||||
// if we don't have keytab or principal only option is to rely on
|
||||
// credentials cache.
|
||||
if (!StringUtils.hasText(this.keyTabLocation) || !StringUtils.hasText(this.userPrincipal)) {
|
||||
// cache
|
||||
options.put("useTicketCache", "true");
|
||||
}
|
||||
else {
|
||||
// keytab
|
||||
options.put("useKeyTab", "true");
|
||||
options.put("keyTab", this.keyTabLocation);
|
||||
options.put("principal", this.userPrincipal);
|
||||
options.put("storeKey", "true");
|
||||
}
|
||||
|
||||
options.put("doNotPrompt", Boolean.toString(this.password == null));
|
||||
options.put("isInitiator", "true");
|
||||
|
||||
if (this.loginOptions != null) {
|
||||
options.putAll(this.loginOptions);
|
||||
}
|
||||
|
||||
return new AppConfigurationEntry[] {
|
||||
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NullCredentials implements Credentials {
|
||||
|
||||
@Override
|
||||
public Principal getUserPrincipal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] getPassword() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class CallbackHandlerImpl implements CallbackHandler {
|
||||
|
||||
private final String userPrincipal;
|
||||
|
||||
private final String password;
|
||||
|
||||
private CallbackHandlerImpl(String userPrincipal, String password) {
|
||||
super();
|
||||
this.userPrincipal = userPrincipal;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof NameCallback) {
|
||||
NameCallback nc = (NameCallback) callback;
|
||||
nc.setName(this.userPrincipal);
|
||||
}
|
||||
else if (callback instanceof PasswordCallback) {
|
||||
PasswordCallback pc = (PasswordCallback) callback;
|
||||
pc.setPassword(this.password.toCharArray());
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedCallbackException(callback, "Unknown Callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.client.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Configuration} which uses Sun's JAAS Krb5LoginModule.
|
||||
*
|
||||
* @author Nelson Rodrigues
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
*/
|
||||
public class SunJaasKrb5LoginConfig extends Configuration implements InitializingBean {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(SunJaasKrb5LoginConfig.class);
|
||||
|
||||
private String servicePrincipal;
|
||||
|
||||
private Resource keyTabLocation;
|
||||
|
||||
private Boolean useTicketCache = false;
|
||||
|
||||
private Boolean isInitiator = false;
|
||||
|
||||
private Boolean debug = false;
|
||||
|
||||
private String keyTabLocationAsString;
|
||||
|
||||
public void setServicePrincipal(String servicePrincipal) {
|
||||
this.servicePrincipal = servicePrincipal;
|
||||
}
|
||||
|
||||
public void setKeyTabLocation(Resource keyTabLocation) {
|
||||
this.keyTabLocation = keyTabLocation;
|
||||
}
|
||||
|
||||
public void setUseTicketCache(Boolean useTicketCache) {
|
||||
this.useTicketCache = useTicketCache;
|
||||
}
|
||||
|
||||
public void setIsInitiator(Boolean isInitiator) {
|
||||
this.isInitiator = isInitiator;
|
||||
}
|
||||
|
||||
public void setDebug(Boolean debug) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.hasText(this.servicePrincipal, "servicePrincipal must be specified");
|
||||
|
||||
if (this.keyTabLocation != null && this.keyTabLocation instanceof ClassPathResource) {
|
||||
LOG.warn(
|
||||
"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
|
||||
}
|
||||
|
||||
if (!this.useTicketCache) {
|
||||
Assert.notNull(this.keyTabLocation, "keyTabLocation must be specified when useTicketCache is false");
|
||||
}
|
||||
|
||||
if (this.keyTabLocation != null) {
|
||||
this.keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
|
||||
if (this.keyTabLocationAsString.startsWith("file:")) {
|
||||
this.keyTabLocationAsString = this.keyTabLocationAsString.substring(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
HashMap<String, String> options = new HashMap<>();
|
||||
|
||||
options.put("principal", this.servicePrincipal);
|
||||
|
||||
if (this.keyTabLocation != null) {
|
||||
options.put("useKeyTab", "true");
|
||||
options.put("keyTab", this.keyTabLocationAsString);
|
||||
options.put("storeKey", "true");
|
||||
}
|
||||
|
||||
options.put("doNotPrompt", "true");
|
||||
|
||||
if (this.useTicketCache) {
|
||||
options.put("useTicketCache", "true");
|
||||
options.put("renewTGT", "true");
|
||||
}
|
||||
|
||||
options.put("isInitiator", this.isInitiator.toString());
|
||||
options.put("debug", this.debug.toString());
|
||||
|
||||
return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.client.ldap;
|
||||
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.ldap.core.support.LdapContextSource;
|
||||
import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
|
||||
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of an {@link LdapContextSource} that authenticates with the ldap server
|
||||
* using Kerberos.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* <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" />
|
||||
* </pre>
|
||||
*
|
||||
* @author Nelson Rodrigues
|
||||
* @see SunJaasKrb5LoginConfig
|
||||
*/
|
||||
public class KerberosLdapContextSource extends DefaultSpringSecurityContextSource implements InitializingBean {
|
||||
|
||||
private Configuration loginConfig;
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos ldap context source.
|
||||
* @param url the url
|
||||
*/
|
||||
public KerberosLdapContextSource(String url) {
|
||||
super(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new kerberos ldap context source.
|
||||
* @param urls the urls
|
||||
* @param baseDn the base dn
|
||||
*/
|
||||
public KerberosLdapContextSource(List<String> urls, String baseDn) {
|
||||
super(urls, baseDn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() /* throws Exception */ {
|
||||
// org.springframework.ldap.core.support.AbstractContextSource in 4.x
|
||||
// doesn't throw Exception for its InitializingBean method, so
|
||||
// we had to remove it from here also. Addition to that
|
||||
// we need to catch super call and re-throw.
|
||||
try {
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
Assert.notNull(this.loginConfig, "loginConfig must be specified");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected DirContext getDirContextInstance(final @SuppressWarnings("rawtypes") Hashtable environment)
|
||||
throws NamingException {
|
||||
environment.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
|
||||
|
||||
Subject serviceSubject = login();
|
||||
|
||||
final NamingException[] suppressedException = new NamingException[] { null };
|
||||
DirContext dirContext = Subject.doAs(serviceSubject, new PrivilegedAction<>() {
|
||||
|
||||
@Override
|
||||
public DirContext run() {
|
||||
try {
|
||||
return KerberosLdapContextSource.super.getDirContextInstance(environment);
|
||||
}
|
||||
catch (NamingException ex) {
|
||||
suppressedException[0] = ex;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (suppressedException[0] != null) {
|
||||
throw suppressedException[0];
|
||||
}
|
||||
|
||||
return dirContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* The login configuration to get the serviceSubject from LoginContext
|
||||
* @param loginConfig the login config
|
||||
*/
|
||||
public void setLoginConfig(Configuration loginConfig) {
|
||||
this.loginConfig = loginConfig;
|
||||
}
|
||||
|
||||
private Subject login() throws AuthenticationException {
|
||||
try {
|
||||
LoginContext lc = new LoginContext(KerberosLdapContextSource.class.getSimpleName(), null, null,
|
||||
this.loginConfig);
|
||||
|
||||
lc.login();
|
||||
|
||||
return lc.getSubject();
|
||||
}
|
||||
catch (LoginException ex) {
|
||||
AuthenticationException ae = new AuthenticationException(ex.getMessage());
|
||||
ae.initCause(ex);
|
||||
throw ae;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
'}'
|
|
@ -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
|
|
@ -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'
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Holds the Subject of the currently authenticated user, since this Jaas object also has
|
||||
* the credentials, and permits creating new credentials against other Kerberos services.
|
||||
* </p>
|
||||
*
|
||||
* @author Bogdan Mustiata
|
||||
* @see SunJaasKerberosClient
|
||||
* @see org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider
|
||||
*/
|
||||
public class JaasSubjectHolder implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8174713761131577405L;
|
||||
|
||||
private Subject jaasSubject;
|
||||
|
||||
private String username;
|
||||
|
||||
private Map<String, byte[]> savedTokens = new HashMap<String, byte[]>();
|
||||
|
||||
public JaasSubjectHolder(Subject jaasSubject) {
|
||||
this.jaasSubject = jaasSubject;
|
||||
}
|
||||
|
||||
public JaasSubjectHolder(Subject jaasSubject, String username) {
|
||||
this.jaasSubject = jaasSubject;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public Subject getJaasSubject() {
|
||||
return this.jaasSubject;
|
||||
}
|
||||
|
||||
public void addToken(String targetService, byte[] outToken) {
|
||||
this.savedTokens.put(targetService, outToken);
|
||||
}
|
||||
|
||||
public byte[] getToken(String principalName) {
|
||||
return this.savedTokens.get(principalName);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
/**
|
||||
* {@link AuthenticationProvider} for kerberos.
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Bogdan Mustiata
|
||||
* @since 1.0
|
||||
*/
|
||||
public class KerberosAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private KerberosClient kerberosClient;
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
|
||||
JaasSubjectHolder subjectHolder = this.kerberosClient.login(auth.getName(), auth.getCredentials().toString());
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(subjectHolder.getUsername());
|
||||
KerberosUsernamePasswordAuthenticationToken output = new KerberosUsernamePasswordAuthenticationToken(
|
||||
userDetails, auth.getCredentials(), userDetails.getAuthorities(), subjectHolder);
|
||||
output.setDetails(authentication.getDetails());
|
||||
return output;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<? extends Object> authentication) {
|
||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the kerberos client.
|
||||
* @param kerberosClient the new kerberos client
|
||||
*/
|
||||
public void setKerberosClient(KerberosClient kerberosClient) {
|
||||
this.kerberosClient = kerberosClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user details service.
|
||||
* @param detailsService the new user details service
|
||||
*/
|
||||
public void setUserDetailsService(UserDetailsService detailsService) {
|
||||
this.userDetailsService = detailsService;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.security.PrivilegedAction;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
import org.ietf.jgss.GSSContext;
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSException;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.GSSName;
|
||||
import org.ietf.jgss.Oid;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Allows creating tickets against other service principals storing the tickets in the
|
||||
* KerberosAuthentication's JaasSubjectHolder.
|
||||
* </p>
|
||||
*
|
||||
* @author Bogdan Mustiata
|
||||
*/
|
||||
public final class KerberosMultiTier {
|
||||
|
||||
public static final String KERBEROS_OID_STRING = "1.2.840.113554.1.2.2";
|
||||
|
||||
public static final Oid KERBEROS_OID = createOid(KERBEROS_OID_STRING);
|
||||
|
||||
/**
|
||||
* Create a new ticket for the
|
||||
* @param authentication
|
||||
* @param username
|
||||
* @param lifetimeInSeconds
|
||||
* @param targetService
|
||||
* @return
|
||||
*/
|
||||
public static Authentication authenticateService(Authentication authentication, final String username,
|
||||
final int lifetimeInSeconds, final String targetService) {
|
||||
|
||||
KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
|
||||
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
|
||||
Subject subject = jaasSubjectHolder.getJaasSubject();
|
||||
|
||||
Subject.doAs(subject, new PrivilegedAction<Object>() {
|
||||
@Override
|
||||
public Object run() {
|
||||
runAuthentication(jaasSubjectHolder, username, lifetimeInSeconds, targetService);
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return authentication;
|
||||
}
|
||||
|
||||
public static byte[] getTokenForService(Authentication authentication, String principalName) {
|
||||
KerberosAuthentication kerberosAuthentication = (KerberosAuthentication) authentication;
|
||||
final JaasSubjectHolder jaasSubjectHolder = kerberosAuthentication.getJaasSubjectHolder();
|
||||
|
||||
return jaasSubjectHolder.getToken(principalName);
|
||||
}
|
||||
|
||||
private static void runAuthentication(JaasSubjectHolder jaasContext, String username, int lifetimeInSeconds,
|
||||
String targetService) {
|
||||
try {
|
||||
GSSManager manager = GSSManager.getInstance();
|
||||
GSSName clientName = manager.createName(username, GSSName.NT_USER_NAME);
|
||||
|
||||
GSSCredential clientCredential = manager.createCredential(clientName, lifetimeInSeconds, KERBEROS_OID,
|
||||
GSSCredential.INITIATE_ONLY);
|
||||
|
||||
GSSName serverName = manager.createName(targetService, GSSName.NT_USER_NAME);
|
||||
|
||||
GSSContext securityContext = manager.createContext(serverName, KERBEROS_OID, clientCredential,
|
||||
GSSContext.DEFAULT_LIFETIME);
|
||||
|
||||
securityContext.requestCredDeleg(true);
|
||||
securityContext.requestInteg(false);
|
||||
securityContext.requestAnonymity(false);
|
||||
securityContext.requestMutualAuth(false);
|
||||
securityContext.requestReplayDet(false);
|
||||
securityContext.requestSequenceDet(false);
|
||||
|
||||
boolean established = false;
|
||||
|
||||
byte[] outToken = new byte[0];
|
||||
|
||||
while (!established) {
|
||||
byte[] inToken = new byte[0];
|
||||
outToken = securityContext.initSecContext(inToken, 0, inToken.length);
|
||||
|
||||
established = securityContext.isEstablished();
|
||||
}
|
||||
|
||||
jaasContext.addToken(targetService, outToken);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new BadCredentialsException("Kerberos authentication failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Oid createOid(String oid) {
|
||||
try {
|
||||
return new Oid(oid);
|
||||
}
|
||||
catch (GSSException ex) {
|
||||
throw new IllegalStateException("Unable to instantiate Oid: ", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private KerberosMultiTier() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Authentication Provider which validates Kerberos Service Tickets or SPNEGO Tokens
|
||||
* (which includes Kerberos Service Tickets).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It needs a <code>KerberosTicketValidator</code>, which contains the code to validate
|
||||
* the ticket, as this code is different between SUN and IBM JRE.<br>
|
||||
* It also needs an <code>UserDetailsService</code> to load the user properties and the
|
||||
* <code>GrantedAuthorities</code>, as we only get back the username from Kerbeos
|
||||
* </p>
|
||||
*
|
||||
* You can see an example configuration in
|
||||
* <code>SpnegoAuthenticationProcessingFilter</code>.
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Jeremy Stone
|
||||
* @since 1.0
|
||||
* @see KerberosTicketValidator
|
||||
* @see UserDetailsService
|
||||
*/
|
||||
public class KerberosServiceAuthenticationProvider implements AuthenticationProvider, InitializingBean {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(KerberosServiceAuthenticationProvider.class);
|
||||
|
||||
private KerberosTicketValidator ticketValidator;
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication;
|
||||
byte[] token = auth.getToken();
|
||||
LOG.debug("Try to validate Kerberos Token");
|
||||
KerberosTicketValidation ticketValidation = this.ticketValidator.validateTicket(token);
|
||||
LOG.debug("Successfully validated " + ticketValidation.username());
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(ticketValidation.username());
|
||||
this.userDetailsChecker.check(userDetails);
|
||||
additionalAuthenticationChecks(userDetails, auth);
|
||||
KerberosServiceRequestToken responseAuth = new KerberosServiceRequestToken(userDetails, ticketValidation,
|
||||
userDetails.getAuthorities(), token);
|
||||
responseAuth.setDetails(authentication.getDetails());
|
||||
return responseAuth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<? extends Object> auth) {
|
||||
return KerberosServiceRequestToken.class.isAssignableFrom(auth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(this.ticketValidator, "ticketValidator must be specified");
|
||||
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
|
||||
}
|
||||
|
||||
/**
|
||||
* The <code>UserDetailsService</code> to use, for loading the user properties and the
|
||||
* <code>GrantedAuthorities</code>.
|
||||
* @param userDetailsService the new user details service
|
||||
*/
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* The <code>KerberosTicketValidator</code> to use, for validating the Kerberos/SPNEGO
|
||||
* tickets.
|
||||
* @param ticketValidator the new ticket validator
|
||||
*/
|
||||
public void setTicketValidator(KerberosTicketValidator ticketValidator) {
|
||||
this.ticketValidator = ticketValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subclasses to perform any additional checks of a returned
|
||||
* <code>UserDetails</code> for a given authentication request.
|
||||
* @param userDetails as retrieved from the {@link UserDetailsService}
|
||||
* @param authentication validated {@link KerberosServiceRequestToken}
|
||||
* @throws AuthenticationException AuthenticationException if the credentials could
|
||||
* not be validated (generally a <code>BadCredentialsException</code>, an
|
||||
* <code>AuthenticationServiceException</code>)
|
||||
*/
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails, KerberosServiceRequestToken authentication)
|
||||
throws AuthenticationException {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.security.PrivilegedActionException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
import org.ietf.jgss.GSSContext;
|
||||
import org.ietf.jgss.MessageProp;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Holds the Kerberos/SPNEGO token for requesting a kerberized service and is also the
|
||||
* output of <code>KerberosServiceAuthenticationProvider</code>.
|
||||
* </p>
|
||||
* <p>
|
||||
* Will mostly be created in <code>SpnegoAuthenticationProcessingFilter</code> and
|
||||
* authenticated in <code>KerberosServiceAuthenticationProvider</code>.
|
||||
* </p>
|
||||
*
|
||||
* This token cannot be re-authenticated, as you will get a Kerberos Reply error.
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Jeremy Stone
|
||||
* @author Bogdan Mustiata
|
||||
* @since 1.0
|
||||
* @see KerberosServiceAuthenticationProvider
|
||||
*/
|
||||
public class KerberosServiceRequestToken extends AbstractAuthenticationToken implements KerberosAuthentication {
|
||||
|
||||
private static final long serialVersionUID = 395488921064775014L;
|
||||
|
||||
private final byte[] token;
|
||||
|
||||
private final Object principal;
|
||||
|
||||
private final transient KerberosTicketValidation ticketValidation;
|
||||
|
||||
private JaasSubjectHolder jaasSubjectHolder;
|
||||
|
||||
/**
|
||||
* Creates an authenticated token, normally used as an output of an authentication
|
||||
* provider.
|
||||
* @param principal the user principal (mostly of instance <code>UserDetails</code>)
|
||||
* @param ticketValidation result of ticket validation
|
||||
* @param authorities the authorities which are granted to the user
|
||||
* @param token the Kerberos/SPNEGO token
|
||||
* @see UserDetails
|
||||
*/
|
||||
public KerberosServiceRequestToken(Object principal, KerberosTicketValidation ticketValidation,
|
||||
Collection<? extends GrantedAuthority> authorities, byte[] token) {
|
||||
super(authorities);
|
||||
this.token = token;
|
||||
this.principal = principal;
|
||||
this.ticketValidation = ticketValidation;
|
||||
this.jaasSubjectHolder = new JaasSubjectHolder(ticketValidation.subject(), ticketValidation.username());
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unauthenticated instance which should then be authenticated by
|
||||
* <code>KerberosServiceAuthenticationProvider</code>.
|
||||
* @param token Kerberos/SPNEGO token
|
||||
* @see KerberosServiceAuthenticationProvider
|
||||
*/
|
||||
public KerberosServiceRequestToken(byte[] token) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.token = token;
|
||||
this.ticketValidation = null;
|
||||
this.principal = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* equals() is based only on the Kerberos token
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (!super.equals(obj)) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
KerberosServiceRequestToken other = (KerberosServiceRequestToken) obj;
|
||||
if (!Arrays.equals(this.token, other.token)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates hashcode based on the Kerberos token
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = super.hashCode();
|
||||
result = prime * result + Arrays.hashCode(this.token);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Kerberos token
|
||||
* @return the token data
|
||||
*/
|
||||
public byte[] getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ticket validation
|
||||
* @return the ticket validation (which will be null if the token is unauthenticated)
|
||||
*/
|
||||
public KerberosTicketValidation getTicketValidation() {
|
||||
return this.ticketValidation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an authenticated token has a response token
|
||||
* @return whether a response token is available
|
||||
*/
|
||||
public boolean hasResponseToken() {
|
||||
return this.ticketValidation != null && this.ticketValidation.responseToken() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the (Base64) encoded response token assuming one is available.
|
||||
* @return encoded response token
|
||||
*/
|
||||
public String getEncodedResponseToken() {
|
||||
if (!hasResponseToken()) {
|
||||
throw new IllegalStateException("Unauthenticated or no response token");
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(this.ticketValidation.responseToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps an encrypted message using the gss context
|
||||
* @param data the data
|
||||
* @param offset data offset
|
||||
* @param length data length
|
||||
* @return the decrypted message
|
||||
* @throws PrivilegedActionException if jaas throws and error
|
||||
*/
|
||||
public byte[] decrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
|
||||
return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
|
||||
public byte[] run() throws Exception {
|
||||
final GSSContext context = getTicketValidation().getGssContext();
|
||||
return context.unwrap(data, offset, length, new MessageProp(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps an encrypted message using the gss context
|
||||
* @param data the data
|
||||
* @return the decrypted message
|
||||
* @throws PrivilegedActionException if jaas throws and error
|
||||
*/
|
||||
public byte[] decrypt(final byte[] data) throws PrivilegedActionException {
|
||||
return decrypt(data, 0, data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an message using the gss context
|
||||
* @param data the data
|
||||
* @param offset data offset
|
||||
* @param length data length
|
||||
* @return the encrypted message
|
||||
* @throws PrivilegedActionException if jaas throws and error
|
||||
*/
|
||||
public byte[] encrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException {
|
||||
return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction<byte[]>() {
|
||||
public byte[] run() throws Exception {
|
||||
final GSSContext context = getTicketValidation().getGssContext();
|
||||
return context.wrap(data, offset, length, new MessageProp(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an message using the gss context
|
||||
* @param data the data
|
||||
* @return the encrypted message
|
||||
* @throws PrivilegedActionException if jaas throws and error
|
||||
*/
|
||||
public byte[] encrypt(final byte[] data) throws PrivilegedActionException {
|
||||
return encrypt(data, 0, data.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JaasSubjectHolder getJaasSubjectHolder() {
|
||||
return this.jaasSubjectHolder;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
|
||||
import org.ietf.jgss.GSSContext;
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
|
||||
/**
|
||||
* Result of ticket validation
|
||||
*/
|
||||
public final class KerberosTicketValidation {
|
||||
|
||||
private final String username;
|
||||
|
||||
private final Subject subject;
|
||||
|
||||
private final byte[] responseToken;
|
||||
|
||||
private final GSSContext gssContext;
|
||||
|
||||
private final GSSCredential delegationCredential;
|
||||
|
||||
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
|
||||
GSSContext gssContext) {
|
||||
this(username, servicePrincipal, responseToken, gssContext, null);
|
||||
}
|
||||
|
||||
public KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken,
|
||||
GSSContext gssContext, GSSCredential delegationCredential) {
|
||||
final HashSet<KerberosPrincipal> princs = new HashSet<KerberosPrincipal>();
|
||||
princs.add(new KerberosPrincipal(servicePrincipal));
|
||||
|
||||
this.username = username;
|
||||
this.subject = new Subject(false, princs, new HashSet<Object>(), new HashSet<Object>());
|
||||
this.responseToken = responseToken;
|
||||
this.gssContext = gssContext;
|
||||
this.delegationCredential = delegationCredential;
|
||||
}
|
||||
|
||||
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext) {
|
||||
this(username, subject, responseToken, gssContext, null);
|
||||
}
|
||||
|
||||
public KerberosTicketValidation(String username, Subject subject, byte[] responseToken, GSSContext gssContext,
|
||||
GSSCredential delegationCredential) {
|
||||
this.username = username;
|
||||
this.subject = subject;
|
||||
this.responseToken = responseToken;
|
||||
this.gssContext = gssContext;
|
||||
this.delegationCredential = delegationCredential;
|
||||
}
|
||||
|
||||
public String username() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public byte[] responseToken() {
|
||||
return this.responseToken;
|
||||
}
|
||||
|
||||
public GSSContext getGssContext() {
|
||||
return this.gssContext;
|
||||
}
|
||||
|
||||
public Subject subject() {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
public GSSCredential getDelegationCredential() {
|
||||
return this.delegationCredential;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Holds the Username/Password as well as the JAAS Subject allowing multi-tier
|
||||
* authentications using Kerberos.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The JAAS Subject has in its private credentials the Kerberos tickets for generating new
|
||||
* tickets against other service principals using
|
||||
* <code>KerberosMultiTier.authenticateService()</code>
|
||||
* </p>
|
||||
*
|
||||
* @author Bogdan Mustiata
|
||||
* @see KerberosAuthenticationProvider
|
||||
* @see KerberosMultiTier
|
||||
*/
|
||||
public class KerberosUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken
|
||||
implements KerberosAuthentication {
|
||||
|
||||
private static final long serialVersionUID = 6327699460703504153L;
|
||||
|
||||
private final JaasSubjectHolder jaasSubjectHolder;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Creates an authentication token that holds the username and password, and the
|
||||
* Subject that the user will need to create new authentication tokens against other
|
||||
* services.
|
||||
* </p>
|
||||
* @param principal
|
||||
* @param credentials
|
||||
* @param authorities
|
||||
* @param subjectHolder
|
||||
*/
|
||||
public KerberosUsernamePasswordAuthenticationToken(Object principal, Object credentials,
|
||||
Collection<? extends GrantedAuthority> authorities, JaasSubjectHolder subjectHolder) {
|
||||
super(principal, credentials, authorities);
|
||||
this.jaasSubjectHolder = subjectHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JaasSubjectHolder getJaasSubjectHolder() {
|
||||
return this.jaasSubjectHolder;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication.sun;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashSet;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
/**
|
||||
* JAAS utility functions.
|
||||
*
|
||||
* @author Bogdan Mustiata
|
||||
*/
|
||||
public final class JaasUtil {
|
||||
|
||||
/**
|
||||
* Copy the principal and the credentials into a new Subject.
|
||||
* @param subject
|
||||
* @return
|
||||
*/
|
||||
public static Subject copySubject(Subject subject) {
|
||||
Subject subjectCopy = new Subject(false, new HashSet<Principal>(subject.getPrincipals()),
|
||||
new HashSet<Object>(subject.getPublicCredentials()),
|
||||
new HashSet<Object>(subject.getPrivateCredentials()));
|
||||
|
||||
return subjectCopy;
|
||||
}
|
||||
|
||||
private JaasUtil() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication.sun;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.NameCallback;
|
||||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
|
||||
import org.springframework.security.kerberos.authentication.KerberosClient;
|
||||
|
||||
/**
|
||||
* Implementation of {@link KerberosClient} which uses the SUN JAAS login module, which is
|
||||
* included in the SUN JRE, it will not work with an IBM JRE. The whole configuration is
|
||||
* done in this class, no additional JAAS configuration is needed.
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Bogdan Mustiata
|
||||
* @since 1.0
|
||||
*/
|
||||
public class SunJaasKerberosClient implements KerberosClient {
|
||||
|
||||
private boolean debug = false;
|
||||
|
||||
private boolean multiTier = false;
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(SunJaasKerberosClient.class);
|
||||
|
||||
@Override
|
||||
public JaasSubjectHolder login(String username, String password) {
|
||||
LOG.debug("Trying to authenticate " + username + " with Kerberos");
|
||||
JaasSubjectHolder result;
|
||||
|
||||
try {
|
||||
LoginContext loginContext = new LoginContext("", null,
|
||||
new KerberosClientCallbackHandler(username, password), new LoginConfig(this.debug));
|
||||
loginContext.login();
|
||||
|
||||
Subject jaasSubject = loginContext.getSubject();
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Kerberos authenticated user: " + jaasSubject);
|
||||
}
|
||||
|
||||
String validatedUsername = jaasSubject.getPrincipals().iterator().next().toString();
|
||||
Subject subjectCopy = JaasUtil.copySubject(jaasSubject);
|
||||
result = new JaasSubjectHolder(subjectCopy, validatedUsername);
|
||||
|
||||
if (!this.multiTier) {
|
||||
loginContext.logout();
|
||||
}
|
||||
}
|
||||
catch (LoginException ex) {
|
||||
throw new BadCredentialsException("Kerberos authentication failed", ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setDebug(boolean debug) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
public void setMultiTier(boolean multiTier) {
|
||||
this.multiTier = multiTier;
|
||||
}
|
||||
|
||||
private static final class LoginConfig extends Configuration {
|
||||
|
||||
private boolean debug;
|
||||
|
||||
private LoginConfig(boolean debug) {
|
||||
super();
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
HashMap<String, String> options = new HashMap<String, String>();
|
||||
options.put("storeKey", "true");
|
||||
if (this.debug) {
|
||||
options.put("debug", "true");
|
||||
}
|
||||
|
||||
return new AppConfigurationEntry[] {
|
||||
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class KerberosClientCallbackHandler implements CallbackHandler {
|
||||
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private KerberosClientCallbackHandler(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof NameCallback) {
|
||||
NameCallback ncb = (NameCallback) callback;
|
||||
ncb.setName(this.username);
|
||||
}
|
||||
else if (callback instanceof PasswordCallback) {
|
||||
PasswordCallback pwcb = (PasswordCallback) callback;
|
||||
pwcb.setPassword(this.password.toCharArray());
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedCallbackException(callback,
|
||||
"We got a " + callback.getClass().getCanonicalName()
|
||||
+ ", but only NameCallback and PasswordCallback is supported");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication.sun;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.security.PrivilegedActionException;
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
|
||||
import com.sun.security.jgss.GSSUtil;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.ietf.jgss.GSSContext;
|
||||
import org.ietf.jgss.GSSCredential;
|
||||
import org.ietf.jgss.GSSManager;
|
||||
import org.ietf.jgss.GSSName;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.kerberos.authentication.JaasSubjectHolder;
|
||||
import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
|
||||
import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link KerberosTicketValidator} which uses the SUN JAAS login module,
|
||||
* which is included in the SUN JRE, it will not work with an IBM JRE. The whole
|
||||
* configuration is done in this class, no additional JAAS configuration is needed.
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Jeremy Stone
|
||||
* @author Bogdan Mustiata
|
||||
* @since 1.0
|
||||
*/
|
||||
public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, InitializingBean {
|
||||
|
||||
private String servicePrincipal;
|
||||
|
||||
private String realmName;
|
||||
|
||||
private Resource keyTabLocation;
|
||||
|
||||
private Subject serviceSubject;
|
||||
|
||||
private boolean holdOnToGSSContext;
|
||||
|
||||
private boolean debug = false;
|
||||
|
||||
private boolean multiTier = false;
|
||||
|
||||
private boolean refreshKrb5Config = false;
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(SunJaasKerberosTicketValidator.class);
|
||||
|
||||
@Override
|
||||
public KerberosTicketValidation validateTicket(byte[] token) {
|
||||
try {
|
||||
if (!this.multiTier) {
|
||||
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
|
||||
}
|
||||
|
||||
Subject subjectCopy = JaasUtil.copySubject(this.serviceSubject);
|
||||
JaasSubjectHolder subjectHolder = new JaasSubjectHolder(subjectCopy);
|
||||
|
||||
return Subject.doAs(subjectHolder.getJaasSubject(), new KerberosMultitierValidateAction(token));
|
||||
|
||||
}
|
||||
catch (PrivilegedActionException ex) {
|
||||
throw new BadCredentialsException("Kerberos validation not successful", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(this.servicePrincipal, "servicePrincipal must be specified");
|
||||
Assert.notNull(this.keyTabLocation, "keyTab must be specified");
|
||||
if (this.keyTabLocation instanceof ClassPathResource) {
|
||||
this.LOG.warn(
|
||||
"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
|
||||
}
|
||||
String keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
|
||||
// We need to remove the file prefix (if there is one), as it is not supported in
|
||||
// Java 7 anymore.
|
||||
// As Java 6 accepts it with and without the prefix, we don't need to check for
|
||||
// Java 7
|
||||
if (keyTabLocationAsString.startsWith("file:")) {
|
||||
keyTabLocationAsString = keyTabLocationAsString.substring(5);
|
||||
}
|
||||
LoginConfig loginConfig = new LoginConfig(keyTabLocationAsString, this.servicePrincipal, this.realmName,
|
||||
this.multiTier, this.debug, this.refreshKrb5Config);
|
||||
Set<Principal> princ = new HashSet<Principal>(1);
|
||||
princ.add(new KerberosPrincipal(this.servicePrincipal));
|
||||
Subject sub = new Subject(false, princ, new HashSet<Object>(), new HashSet<Object>());
|
||||
LoginContext lc = new LoginContext("", sub, null, loginConfig);
|
||||
lc.login();
|
||||
this.serviceSubject = lc.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* The service principal of the application. For web apps this is
|
||||
* <code>HTTP/full-qualified-domain-name@DOMAIN</code>. The keytab must contain the
|
||||
* key for this principal.
|
||||
* @param servicePrincipal service principal to use
|
||||
* @see #setKeyTabLocation(Resource)
|
||||
*/
|
||||
public void setServicePrincipal(String servicePrincipal) {
|
||||
this.servicePrincipal = servicePrincipal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The realm name of the application. For web apps this is <code>DOMAIN</code>
|
||||
* @param realmName
|
||||
*/
|
||||
public void setRealmName(String realmName) {
|
||||
this.realmName = realmName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param multiTier
|
||||
*/
|
||||
public void setMultiTier(boolean multiTier) {
|
||||
this.multiTier = multiTier;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* The location of the keytab. You can use the normale Spring Resource prefixes like
|
||||
* <code>file:</code> or <code>classpath:</code>, but as the file is later on read by
|
||||
* JAAS, we cannot guarantee that <code>classpath</code> works in every environment,
|
||||
* esp. not in Java EE application servers. You should use <code>file:</code> there.
|
||||
*
|
||||
* This file also needs special protection, which is another reason to not include it
|
||||
* in the classpath but rather use <code>file:/etc/http.keytab</code> for example.
|
||||
* @param keyTabLocation The location where the keytab resides
|
||||
*/
|
||||
public void setKeyTabLocation(Resource keyTabLocation) {
|
||||
this.keyTabLocation = keyTabLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the debug mode of the JAAS Kerberos login module.
|
||||
* @param debug default is false
|
||||
*/
|
||||
public void setDebug(boolean debug) {
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to hold on to the {@link GSSContext GSS security context} or
|
||||
* otherwise {@link GSSContext#dispose() dispose} of it immediately (the default
|
||||
* behaviour).
|
||||
* <p>
|
||||
* Holding on to the GSS context allows decrypt and encrypt operations for subsequent
|
||||
* interactions with the principal.
|
||||
* @param holdOnToGSSContext true if should hold on to context
|
||||
*/
|
||||
public void setHoldOnToGSSContext(boolean holdOnToGSSContext) {
|
||||
this.holdOnToGSSContext = holdOnToGSSContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables configuration to be refreshed before the login method is called.
|
||||
* @param refreshKrb5Config Set this to true, if you want the configuration to be
|
||||
* refreshed before the login method is called.
|
||||
*/
|
||||
public void setRefreshKrb5Config(boolean refreshKrb5Config) {
|
||||
this.refreshKrb5Config = refreshKrb5Config;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is needed, because the validation must run with previously generated
|
||||
* JAAS subject which belongs to the service principal and was loaded out of the
|
||||
* keytab during startup.
|
||||
*/
|
||||
private final class KerberosMultitierValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
|
||||
|
||||
byte[] kerberosTicket;
|
||||
|
||||
private KerberosMultitierValidateAction(byte[] kerberosTicket) {
|
||||
this.kerberosTicket = kerberosTicket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KerberosTicketValidation run() throws Exception {
|
||||
byte[] responseToken = new byte[0];
|
||||
GSSManager manager = GSSManager.getInstance();
|
||||
|
||||
GSSContext context = manager.createContext((GSSCredential) null);
|
||||
|
||||
while (!context.isEstablished()) {
|
||||
context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
|
||||
}
|
||||
|
||||
Subject subject = GSSUtil.createSubject(context.getSrcName(), context.getDelegCred());
|
||||
|
||||
KerberosTicketValidation result = new KerberosTicketValidation(context.getSrcName().toString(), subject,
|
||||
responseToken, context);
|
||||
|
||||
if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
|
||||
context.dispose();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is needed, because the validation must run with previously generated
|
||||
* JAAS subject which belongs to the service principal and was loaded out of the
|
||||
* keytab during startup.
|
||||
*/
|
||||
private final class KerberosValidateAction implements PrivilegedExceptionAction<KerberosTicketValidation> {
|
||||
|
||||
byte[] kerberosTicket;
|
||||
|
||||
private KerberosValidateAction(byte[] kerberosTicket) {
|
||||
this.kerberosTicket = kerberosTicket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KerberosTicketValidation run() throws Exception {
|
||||
byte[] responseToken = new byte[0];
|
||||
GSSName gssName = null;
|
||||
GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null);
|
||||
while (!context.isEstablished()) {
|
||||
responseToken = context.acceptSecContext(this.kerberosTicket, 0, this.kerberosTicket.length);
|
||||
gssName = context.getSrcName();
|
||||
if (gssName == null) {
|
||||
throw new BadCredentialsException("GSSContext name of the context initiator is null");
|
||||
}
|
||||
}
|
||||
|
||||
GSSCredential delegationCredential = null;
|
||||
if (context.getCredDelegState()) {
|
||||
delegationCredential = context.getDelegCred();
|
||||
}
|
||||
|
||||
if (!SunJaasKerberosTicketValidator.this.holdOnToGSSContext) {
|
||||
context.dispose();
|
||||
}
|
||||
return new KerberosTicketValidation(gssName.toString(),
|
||||
SunJaasKerberosTicketValidator.this.servicePrincipal, responseToken, context, delegationCredential);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Normally you need a JAAS config file in order to use the JAAS Kerberos Login
|
||||
* Module, with this class it is not needed and you can have different configurations
|
||||
* in one JVM.
|
||||
*/
|
||||
private static final class LoginConfig extends Configuration {
|
||||
|
||||
private String keyTabLocation;
|
||||
|
||||
private String servicePrincipalName;
|
||||
|
||||
private String realmName;
|
||||
|
||||
private boolean multiTier;
|
||||
|
||||
private boolean debug;
|
||||
|
||||
private boolean refreshKrb5Config;
|
||||
|
||||
private LoginConfig(String keyTabLocation, String servicePrincipalName, String realmName, boolean multiTier,
|
||||
boolean debug, boolean refreshKrb5Config) {
|
||||
this.keyTabLocation = keyTabLocation;
|
||||
this.servicePrincipalName = servicePrincipalName;
|
||||
this.realmName = realmName;
|
||||
this.multiTier = multiTier;
|
||||
this.debug = debug;
|
||||
this.refreshKrb5Config = refreshKrb5Config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
HashMap<String, String> options = new HashMap<String, String>();
|
||||
options.put("useKeyTab", "true");
|
||||
options.put("keyTab", this.keyTabLocation);
|
||||
options.put("principal", this.servicePrincipalName);
|
||||
options.put("storeKey", "true");
|
||||
options.put("doNotPrompt", "true");
|
||||
if (this.debug) {
|
||||
options.put("debug", "true");
|
||||
}
|
||||
|
||||
if (this.realmName != null) {
|
||||
options.put("realm", this.realmName);
|
||||
}
|
||||
|
||||
if (this.refreshKrb5Config) {
|
||||
options.put("refreshKrb5Config", "true");
|
||||
}
|
||||
|
||||
if (!this.multiTier) {
|
||||
options.put("isInitiator", "false");
|
||||
}
|
||||
|
||||
return new AppConfigurationEntry[] {
|
||||
new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test class for {@link KerberosAuthenticationProvider}
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @since 1.0
|
||||
*/
|
||||
public class KerberosAuthenticationProviderTests {
|
||||
|
||||
private KerberosAuthenticationProvider provider;
|
||||
|
||||
private KerberosClient kerberosClient;
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
|
||||
|
||||
private static final String TEST_PASSWORD = "password";
|
||||
|
||||
private static final UsernamePasswordAuthenticationToken INPUT_TOKEN = new UsernamePasswordAuthenticationToken(
|
||||
TEST_USER, TEST_PASSWORD);
|
||||
|
||||
private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
|
||||
|
||||
private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
|
||||
AUTHORITY_LIST);
|
||||
|
||||
private static final JaasSubjectHolder JAAS_SUBJECT_HOLDER = new JaasSubjectHolder(null, TEST_USER);
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
// mocking
|
||||
this.kerberosClient = mock(KerberosClient.class);
|
||||
this.userDetailsService = mock(UserDetailsService.class);
|
||||
this.provider = new KerberosAuthenticationProvider();
|
||||
this.provider.setKerberosClient(this.kerberosClient);
|
||||
this.provider.setUserDetailsService(this.userDetailsService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginOk() throws Exception {
|
||||
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(USER_DETAILS);
|
||||
given(this.kerberosClient.login(TEST_USER, TEST_PASSWORD)).willReturn(JAAS_SUBJECT_HOLDER);
|
||||
|
||||
Authentication authenticate = this.provider.authenticate(INPUT_TOKEN);
|
||||
|
||||
verify(this.kerberosClient).login(TEST_USER, TEST_PASSWORD);
|
||||
|
||||
assertThat(authenticate).isNotNull();
|
||||
assertThat(authenticate.getName()).isEqualTo(TEST_USER);
|
||||
assertThat(authenticate.getPrincipal()).isEqualTo(USER_DETAILS);
|
||||
assertThat(authenticate.getCredentials()).isEqualTo(TEST_PASSWORD);
|
||||
assertThat(authenticate.getAuthorities()).isEqualTo(AUTHORITY_LIST);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.authentication;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Test class for {@link KerberosServiceAuthenticationProvider}
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Jeremy Stone
|
||||
* @since 1.0
|
||||
*/
|
||||
public class KerberosServiceAuthenticationProviderTests {
|
||||
|
||||
private KerberosServiceAuthenticationProvider provider;
|
||||
|
||||
private KerberosTicketValidator ticketValidator;
|
||||
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
// data
|
||||
private static final byte[] TEST_TOKEN = "TestToken".getBytes();
|
||||
|
||||
private static final byte[] RESPONSE_TOKEN = "ResponseToken".getBytes();
|
||||
|
||||
private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG";
|
||||
|
||||
private static final KerberosTicketValidation TICKET_VALIDATION = new KerberosTicketValidation(TEST_USER,
|
||||
"XXX@test.com", RESPONSE_TOKEN, null);
|
||||
|
||||
private static final List<GrantedAuthority> AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
|
||||
|
||||
private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true, true,
|
||||
AUTHORITY_LIST);
|
||||
|
||||
private static final KerberosServiceRequestToken INPUT_TOKEN = new KerberosServiceRequestToken(TEST_TOKEN);
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
System.setProperty("java.security.krb5.conf", "test.com");
|
||||
System.setProperty("java.security.krb5.kdc", "kdc.test.com");
|
||||
// mocking
|
||||
this.ticketValidator = mock(KerberosTicketValidator.class);
|
||||
this.userDetailsService = mock(UserDetailsService.class);
|
||||
this.provider = new KerberosServiceAuthenticationProvider();
|
||||
this.provider.setTicketValidator(this.ticketValidator);
|
||||
this.provider.setUserDetailsService(this.userDetailsService);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
System.clearProperty("java.security.krb5.conf");
|
||||
System.clearProperty("java.security.krb5.kdc");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEverythingWorks() throws Exception {
|
||||
Authentication output = callProviderAndReturnUser(USER_DETAILS, INPUT_TOKEN);
|
||||
assertThat(output).isNotNull();
|
||||
assertThat(output.getName()).isEqualTo(TEST_USER);
|
||||
assertThat(output.getAuthorities()).isEqualTo(AUTHORITY_LIST);
|
||||
assertThat(output.getPrincipal()).isEqualTo(USER_DETAILS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationDetailsPropagation() throws Exception {
|
||||
KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN);
|
||||
requestToken.setDetails("TestDetails");
|
||||
Authentication output = callProviderAndReturnUser(USER_DETAILS, requestToken);
|
||||
assertThat(output).isNotNull();
|
||||
assertThat(output.getDetails()).isEqualTo(requestToken.getDetails());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserIsDisabled() throws Exception {
|
||||
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> {
|
||||
User disabledUser = new User(TEST_USER, "empty", false, true, true, true, AUTHORITY_LIST);
|
||||
callProviderAndReturnUser(disabledUser, INPUT_TOKEN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserAccountIsExpired() throws Exception {
|
||||
assertThatExceptionOfType(AccountExpiredException.class).isThrownBy(() -> {
|
||||
User expiredUser = new User(TEST_USER, "empty", true, false, true, true, AUTHORITY_LIST);
|
||||
callProviderAndReturnUser(expiredUser, INPUT_TOKEN);
|
||||
}).isInstanceOf(AccountExpiredException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserCredentialsExpired() throws Exception {
|
||||
assertThatExceptionOfType(CredentialsExpiredException.class).isThrownBy(() -> {
|
||||
User credExpiredUser = new User(TEST_USER, "empty", true, true, false, true, AUTHORITY_LIST);
|
||||
callProviderAndReturnUser(credExpiredUser, INPUT_TOKEN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserAccountLockedCredentialsExpired() throws Exception {
|
||||
assertThatExceptionOfType(LockedException.class).isThrownBy(() -> {
|
||||
User lockedUser = new User(TEST_USER, "empty", true, true, true, false, AUTHORITY_LIST);
|
||||
callProviderAndReturnUser(lockedUser, INPUT_TOKEN);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsernameNotFound() throws Exception {
|
||||
// stubbing
|
||||
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
|
||||
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willThrow(new UsernameNotFoundException(""));
|
||||
|
||||
// testing
|
||||
assertThatExceptionOfType(UsernameNotFoundException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTicketValidationWrong() throws Exception {
|
||||
// stubbing
|
||||
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willThrow(new BadCredentialsException(""));
|
||||
|
||||
// testing
|
||||
assertThatExceptionOfType(BadCredentialsException.class)
|
||||
.isThrownBy(() -> this.provider.authenticate(INPUT_TOKEN));
|
||||
}
|
||||
|
||||
private Authentication callProviderAndReturnUser(UserDetails userDetails, Authentication inputToken) {
|
||||
// stubbing
|
||||
given(this.ticketValidator.validateTicket(TEST_TOKEN)).willReturn(TICKET_VALIDATION);
|
||||
given(this.userDetailsService.loadUserByUsername(TEST_USER)).willReturn(userDetails);
|
||||
|
||||
// testing
|
||||
return this.provider.authenticate(inputToken);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
/**
|
||||
* KerberosSecurityTestcase provides a base class for using MiniKdc with other testcases.
|
||||
* KerberosSecurityTestcase starts the MiniKdc (@Before) before running tests, and stop
|
||||
* the MiniKdc (@After) after the testcases, using default settings (working dir and kdc
|
||||
* configurations).
|
||||
* <p>
|
||||
* Users can directly inherit this class and implement their own test functions using the
|
||||
* default settings, or override functions getTestDir() and createMiniKdcConf() to provide
|
||||
* new settings.
|
||||
*
|
||||
*/
|
||||
public class KerberosSecurityTestcase {
|
||||
|
||||
private MiniKdc kdc;
|
||||
|
||||
private File workDir;
|
||||
|
||||
private Properties conf;
|
||||
|
||||
@BeforeEach
|
||||
public void startMiniKdc() throws Exception {
|
||||
createTestDir();
|
||||
createMiniKdcConf();
|
||||
|
||||
this.kdc = new MiniKdc(this.conf, this.workDir);
|
||||
this.kdc.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a working directory, it should be the build directory. Under this directory
|
||||
* an ApacheDS working directory will be created, this directory will be deleted when
|
||||
* the MiniKdc stops.
|
||||
*/
|
||||
public void createTestDir() {
|
||||
this.workDir = new File(System.getProperty("test.dir", "target"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Kdc configuration
|
||||
*/
|
||||
public void createMiniKdcConf() {
|
||||
this.conf = MiniKdc.createConf();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stopMiniKdc() {
|
||||
if (this.kdc != null) {
|
||||
this.kdc.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public MiniKdc getKdc() {
|
||||
return this.kdc;
|
||||
}
|
||||
|
||||
public File getWorkDir() {
|
||||
return this.workDir;
|
||||
}
|
||||
|
||||
public Properties getConf() {
|
||||
return this.conf;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.kerby.kerberos.kerb.KrbException;
|
||||
import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
|
||||
import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
|
||||
import org.apache.kerby.util.IOUtil;
|
||||
import org.apache.kerby.util.NetworkUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Mini KDC based on Apache Directory Server that can be embedded in testcases or used
|
||||
* from command line as a standalone KDC.
|
||||
* <p>
|
||||
* <b>From within testcases:</b>
|
||||
* <p>
|
||||
* MiniKdc sets one System property when started and un-set when stopped:
|
||||
* <ul>
|
||||
* <li>sun.security.krb5.debug: set to the debug value provided in the configuration</li>
|
||||
* </ul>
|
||||
* Because of this, multiple MiniKdc instances cannot be started in parallel. For example,
|
||||
* running testcases in parallel that start a KDC each. To accomplish this a single
|
||||
* MiniKdc should be used for all testcases running in parallel.
|
||||
* <p>
|
||||
* MiniKdc default configuration values are:
|
||||
* <ul>
|
||||
* <li>org.name=EXAMPLE (used to create the REALM)</li>
|
||||
* <li>org.domain=COM (used to create the REALM)</li>
|
||||
* <li>kdc.bind.address=localhost</li>
|
||||
* <li>kdc.port=0 (ephemeral port)</li>
|
||||
* <li>instance=DefaultKrbServer</li>
|
||||
* <li>max.ticket.lifetime=86400000 (1 day)</li>
|
||||
* <li>max.renewable.lifetime=604800000 (7 days)</li>
|
||||
* <li>transport=TCP</li>
|
||||
* <li>debug=false</li>
|
||||
* </ul>
|
||||
* The generated krb5.conf forces TCP connections.
|
||||
*
|
||||
* @author Original Hadoop MiniKdc Authors
|
||||
* @author Janne Valkealahti
|
||||
* @author Bogdan Mustiata
|
||||
*/
|
||||
public class MiniKdc {
|
||||
|
||||
public static final String JAVA_SECURITY_KRB5_CONF = "java.security.krb5.conf";
|
||||
|
||||
public static final String SUN_SECURITY_KRB5_DEBUG = "sun.security.krb5.debug";
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length < 4) {
|
||||
System.out.println("Arguments: <WORKDIR> <MINIKDCPROPERTIES> " + "<KEYTABFILE> [<PRINCIPALS>]+");
|
||||
System.exit(1);
|
||||
}
|
||||
File workDir = new File(args[0]);
|
||||
if (!workDir.exists()) {
|
||||
throw new RuntimeException("Specified work directory does not exists: " + workDir.getAbsolutePath());
|
||||
}
|
||||
Properties conf = createConf();
|
||||
File file = new File(args[1]);
|
||||
if (!file.exists()) {
|
||||
throw new RuntimeException("Specified configuration does not exists: " + file.getAbsolutePath());
|
||||
}
|
||||
Properties userConf = new Properties();
|
||||
InputStreamReader r = null;
|
||||
try {
|
||||
r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
|
||||
userConf.load(r);
|
||||
}
|
||||
finally {
|
||||
if (r != null) {
|
||||
r.close();
|
||||
}
|
||||
}
|
||||
for (Map.Entry<?, ?> entry : userConf.entrySet()) {
|
||||
conf.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
final MiniKdc miniKdc = new MiniKdc(conf, workDir);
|
||||
miniKdc.start();
|
||||
File krb5conf = new File(workDir, "krb5.conf");
|
||||
if (miniKdc.getKrb5conf().renameTo(krb5conf)) {
|
||||
File keytabFile = new File(args[2]).getAbsoluteFile();
|
||||
String[] principals = new String[args.length - 3];
|
||||
System.arraycopy(args, 3, principals, 0, args.length - 3);
|
||||
miniKdc.createPrincipal(keytabFile, principals);
|
||||
System.out.println();
|
||||
System.out.println("Standalone MiniKdc Running");
|
||||
System.out.println("---------------------------------------------------");
|
||||
System.out.println(" Realm : " + miniKdc.getRealm());
|
||||
System.out.println(" Running at : " + miniKdc.getHost() + ":" + miniKdc.getPort());
|
||||
System.out.println(" krb5conf : " + krb5conf);
|
||||
System.out.println();
|
||||
System.out.println(" created keytab : " + keytabFile);
|
||||
System.out.println(" with principals : " + Arrays.asList(principals));
|
||||
System.out.println();
|
||||
System.out.println(" Do <CTRL-C> or kill <PID> to stop it");
|
||||
System.out.println("---------------------------------------------------");
|
||||
System.out.println();
|
||||
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
miniKdc.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Cannot rename KDC's krb5conf to " + krb5conf.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class);
|
||||
|
||||
public static final String ORG_NAME = "org.name";
|
||||
|
||||
public static final String ORG_DOMAIN = "org.domain";
|
||||
|
||||
public static final String KDC_BIND_ADDRESS = "kdc.bind.address";
|
||||
|
||||
public static final String KDC_PORT = "kdc.port";
|
||||
|
||||
public static final String INSTANCE = "instance";
|
||||
|
||||
public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime";
|
||||
|
||||
public static final String MIN_TICKET_LIFETIME = "min.ticket.lifetime";
|
||||
|
||||
public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime";
|
||||
|
||||
public static final String TRANSPORT = "transport";
|
||||
|
||||
public static final String DEBUG = "debug";
|
||||
|
||||
private static final Set<String> PROPERTIES = new HashSet<String>();
|
||||
|
||||
private static final Properties DEFAULT_CONFIG = new Properties();
|
||||
|
||||
static {
|
||||
PROPERTIES.add(ORG_NAME);
|
||||
PROPERTIES.add(ORG_DOMAIN);
|
||||
PROPERTIES.add(KDC_BIND_ADDRESS);
|
||||
PROPERTIES.add(KDC_BIND_ADDRESS);
|
||||
PROPERTIES.add(KDC_PORT);
|
||||
PROPERTIES.add(INSTANCE);
|
||||
PROPERTIES.add(TRANSPORT);
|
||||
PROPERTIES.add(MAX_TICKET_LIFETIME);
|
||||
PROPERTIES.add(MAX_RENEWABLE_LIFETIME);
|
||||
|
||||
DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost");
|
||||
DEFAULT_CONFIG.setProperty(KDC_PORT, "0");
|
||||
DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer");
|
||||
DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE");
|
||||
DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM");
|
||||
DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP");
|
||||
DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000");
|
||||
DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000");
|
||||
DEFAULT_CONFIG.setProperty(DEBUG, "false");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that returns MiniKdc default configuration.
|
||||
* <p>
|
||||
* The returned configuration is a copy, it can be customized before using it to
|
||||
* create a MiniKdc.
|
||||
* @return a MiniKdc default configuration.
|
||||
*/
|
||||
public static Properties createConf() {
|
||||
return (Properties) DEFAULT_CONFIG.clone();
|
||||
}
|
||||
|
||||
private Properties conf;
|
||||
|
||||
private SimpleKdcServer simpleKdc;
|
||||
|
||||
private int port;
|
||||
|
||||
private String realm;
|
||||
|
||||
private File workDir;
|
||||
|
||||
private File krb5conf;
|
||||
|
||||
private String transport;
|
||||
|
||||
private boolean krb5Debug;
|
||||
|
||||
public void setTransport(String transport) {
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MiniKdc.
|
||||
* @param conf MiniKdc configuration.
|
||||
* @param workDir working directory, it should be the build directory. Under this
|
||||
* directory an ApacheDS working directory will be created, this directory will be
|
||||
* deleted when the MiniKdc stops.
|
||||
* @throws Exception thrown if the MiniKdc could not be created.
|
||||
*/
|
||||
public MiniKdc(Properties conf, File workDir) throws Exception {
|
||||
if (!conf.keySet().containsAll(PROPERTIES)) {
|
||||
Set<String> missingProperties = new HashSet<String>(PROPERTIES);
|
||||
missingProperties.removeAll(conf.keySet());
|
||||
throw new IllegalArgumentException("Missing configuration properties: " + missingProperties);
|
||||
}
|
||||
this.workDir = new File(workDir, Long.toString(System.currentTimeMillis()));
|
||||
if (!this.workDir.exists() && !this.workDir.mkdirs()) {
|
||||
throw new RuntimeException("Cannot create directory " + this.workDir);
|
||||
}
|
||||
LOG.info("Configuration:");
|
||||
LOG.info("---------------------------------------------------------------");
|
||||
for (Map.Entry<?, ?> entry : conf.entrySet()) {
|
||||
LOG.info(" {}: {}", entry.getKey(), entry.getValue());
|
||||
}
|
||||
LOG.info("---------------------------------------------------------------");
|
||||
this.conf = conf;
|
||||
this.port = Integer.parseInt(conf.getProperty(KDC_PORT));
|
||||
String orgName = conf.getProperty(ORG_NAME);
|
||||
String orgDomain = conf.getProperty(ORG_DOMAIN);
|
||||
this.realm = orgName.toUpperCase(Locale.ENGLISH) + "." + orgDomain.toUpperCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the port of the MiniKdc.
|
||||
* @return the port of the MiniKdc.
|
||||
*/
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the host of the MiniKdc.
|
||||
* @return the host of the MiniKdc.
|
||||
*/
|
||||
public String getHost() {
|
||||
return this.conf.getProperty(KDC_BIND_ADDRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the realm of the MiniKdc.
|
||||
* @return the realm of the MiniKdc.
|
||||
*/
|
||||
public String getRealm() {
|
||||
return this.realm;
|
||||
}
|
||||
|
||||
public File getKrb5conf() {
|
||||
this.krb5conf = new File(System.getProperty(JAVA_SECURITY_KRB5_CONF));
|
||||
return this.krb5conf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the MiniKdc.
|
||||
* @throws Exception thrown if the MiniKdc could not be started.
|
||||
*/
|
||||
public synchronized void start() throws Exception {
|
||||
if (this.simpleKdc != null) {
|
||||
throw new RuntimeException("Already started");
|
||||
}
|
||||
this.simpleKdc = new SimpleKdcServer();
|
||||
prepareKdcServer();
|
||||
this.simpleKdc.init();
|
||||
resetDefaultRealm();
|
||||
this.simpleKdc.start();
|
||||
LOG.info("MiniKdc started.");
|
||||
}
|
||||
|
||||
private void resetDefaultRealm() throws IOException {
|
||||
InputStream templateResource = new FileInputStream(getKrb5conf().getAbsolutePath());
|
||||
String content = IOUtil.readInput(templateResource);
|
||||
content = content.replaceAll("default_realm = .*\n", "default_realm = " + getRealm() + "\n");
|
||||
IOUtil.writeFile(content, getKrb5conf());
|
||||
}
|
||||
|
||||
private void prepareKdcServer() throws Exception {
|
||||
// transport
|
||||
this.simpleKdc.setWorkDir(this.workDir);
|
||||
this.simpleKdc.setKdcHost(getHost());
|
||||
this.simpleKdc.setKdcRealm(this.realm);
|
||||
if (this.transport == null) {
|
||||
this.transport = this.conf.getProperty(TRANSPORT);
|
||||
}
|
||||
if (this.port == 0) {
|
||||
this.port = NetworkUtil.getServerPort();
|
||||
}
|
||||
if (this.transport != null) {
|
||||
if (this.transport.trim().equals("TCP")) {
|
||||
this.simpleKdc.setKdcTcpPort(this.port);
|
||||
this.simpleKdc.setAllowUdp(false);
|
||||
}
|
||||
else if (this.transport.trim().equals("UDP")) {
|
||||
this.simpleKdc.setKdcUdpPort(this.port);
|
||||
this.simpleKdc.setAllowTcp(false);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Invalid transport: " + this.transport);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Need to set transport!");
|
||||
}
|
||||
this.simpleKdc.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, this.conf.getProperty(INSTANCE));
|
||||
if (this.conf.getProperty(DEBUG) != null) {
|
||||
this.krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG, this.conf.getProperty(DEBUG));
|
||||
}
|
||||
if (this.conf.getProperty(MIN_TICKET_LIFETIME) != null) {
|
||||
this.simpleKdc.getKdcConfig()
|
||||
.setLong(KdcConfigKey.MINIMUM_TICKET_LIFETIME,
|
||||
Long.parseLong(this.conf.getProperty(MIN_TICKET_LIFETIME)));
|
||||
}
|
||||
if (this.conf.getProperty(MAX_TICKET_LIFETIME) != null) {
|
||||
this.simpleKdc.getKdcConfig()
|
||||
.setLong(KdcConfigKey.MAXIMUM_TICKET_LIFETIME,
|
||||
Long.parseLong(this.conf.getProperty(MiniKdc.MAX_TICKET_LIFETIME)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the MiniKdc
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
if (this.simpleKdc != null) {
|
||||
try {
|
||||
this.simpleKdc.stop();
|
||||
}
|
||||
catch (KrbException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
finally {
|
||||
if (this.conf.getProperty(DEBUG) != null) {
|
||||
System.setProperty(SUN_SECURITY_KRB5_DEBUG, Boolean.toString(this.krb5Debug));
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(this.workDir);
|
||||
try {
|
||||
// Will be fixed in next Kerby version.
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
LOG.info("MiniKdc stopped.");
|
||||
}
|
||||
|
||||
private void delete(File f) {
|
||||
if (f.isFile()) {
|
||||
if (!f.delete()) {
|
||||
LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
else {
|
||||
File[] fileList = f.listFiles();
|
||||
if (fileList != null) {
|
||||
for (File c : fileList) {
|
||||
delete(c);
|
||||
}
|
||||
}
|
||||
if (!f.delete()) {
|
||||
LOG.warn("WARNING: cannot delete directory " + f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a principal in the KDC with the specified user and password.
|
||||
* @param principal principal name, do not include the domain.
|
||||
* @param password password.
|
||||
* @throws Exception thrown if the principal could not be created.
|
||||
*/
|
||||
public synchronized void createPrincipal(String principal, String password) throws Exception {
|
||||
this.simpleKdc.createPrincipal(principal, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple principals in the KDC and adds them to a keytab file.
|
||||
* @param keytabFile keytab file to add the created principals.
|
||||
* @param principals principals to add to the KDC, do not include the domain.
|
||||
* @throws Exception thrown if the principals or the keytab file could not be created.
|
||||
*/
|
||||
public synchronized void createPrincipal(File keytabFile, String... principals) throws Exception {
|
||||
this.simpleKdc.createPrincipals(principals);
|
||||
if (keytabFile.exists() && !keytabFile.delete()) {
|
||||
LOG.error("Failed to delete keytab file: " + keytabFile);
|
||||
}
|
||||
for (String principal : principals) {
|
||||
this.simpleKdc.getKadmin().exportKeytab(keytabFile, principal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the System property; return the old value for caching.
|
||||
* @param sysprop property
|
||||
* @param debug true or false
|
||||
* @return the previous value
|
||||
*/
|
||||
private boolean getAndSet(String sysprop, String debug) {
|
||||
boolean old = Boolean.getBoolean(sysprop);
|
||||
System.setProperty(sysprop, debug);
|
||||
return old;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
import javax.security.auth.login.AppConfigurationEntry;
|
||||
import javax.security.auth.login.Configuration;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
|
||||
import org.apache.kerby.kerberos.kerb.keytab.Keytab;
|
||||
import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class TestMiniKdc extends KerberosSecurityTestcase {
|
||||
|
||||
private static final boolean IBM_JAVA = shouldUseIbmPackages();
|
||||
|
||||
// duplicated to avoid cycles in the build
|
||||
private static boolean shouldUseIbmPackages() {
|
||||
final List<String> ibmTechnologyEditionSecurityModules = Arrays.asList(
|
||||
"com.ibm.security.auth.module.JAASLoginModule", "com.ibm.security.auth.module.Win64LoginModule",
|
||||
"com.ibm.security.auth.module.NTLoginModule", "com.ibm.security.auth.module.AIX64LoginModule",
|
||||
"com.ibm.security.auth.module.LinuxLoginModule", "com.ibm.security.auth.module.Krb5LoginModule");
|
||||
|
||||
if (System.getProperty("java.vendor").contains("IBM")) {
|
||||
return ibmTechnologyEditionSecurityModules.stream().anyMatch((module) -> isSystemClassAvailable(module));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKerberosLogin() throws Exception {
|
||||
MiniKdc kdc = getKdc();
|
||||
File workDir = getWorkDir();
|
||||
LoginContext loginContext = null;
|
||||
try {
|
||||
String principal = "foo";
|
||||
File keytab = new File(workDir, "foo.keytab");
|
||||
kdc.createPrincipal(keytab, principal);
|
||||
|
||||
Set<Principal> principals = new HashSet<Principal>();
|
||||
principals.add(new KerberosPrincipal(principal));
|
||||
|
||||
// client login
|
||||
Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
|
||||
loginContext = new LoginContext("", subject, null,
|
||||
KerberosConfiguration.createClientConfig(principal, keytab));
|
||||
loginContext.login();
|
||||
subject = loginContext.getSubject();
|
||||
assertThat(subject.getPrincipals().size()).isEqualTo(1);
|
||||
assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
|
||||
assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
|
||||
loginContext.logout();
|
||||
|
||||
// server login
|
||||
subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());
|
||||
loginContext = new LoginContext("", subject, null,
|
||||
KerberosConfiguration.createServerConfig(principal, keytab));
|
||||
loginContext.login();
|
||||
subject = loginContext.getSubject();
|
||||
assertThat(subject.getPrincipals().size()).isEqualTo(1);
|
||||
assertThat(subject.getPrincipals().iterator().next().getClass()).isEqualTo(KerberosPrincipal.class);
|
||||
assertThat(subject.getPrincipals().iterator().next().getName()).isEqualTo(principal + "@" + kdc.getRealm());
|
||||
loginContext.logout();
|
||||
|
||||
}
|
||||
finally {
|
||||
if (loginContext != null && loginContext.getSubject() != null
|
||||
&& !loginContext.getSubject().getPrivateCredentials().isEmpty()) {
|
||||
loginContext.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isSystemClassAvailable(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMiniKdcStart() {
|
||||
MiniKdc kdc = getKdc();
|
||||
assertThat(kdc.getPort()).isNotEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeytabGen() throws Exception {
|
||||
MiniKdc kdc = getKdc();
|
||||
File workDir = getWorkDir();
|
||||
|
||||
kdc.createPrincipal(new File(workDir, "keytab"), "foo/bar", "bar/foo");
|
||||
List<PrincipalName> principalNameList = Keytab.loadKeytab(new File(workDir, "keytab")).getPrincipals();
|
||||
|
||||
Set<String> principals = new HashSet<String>();
|
||||
for (PrincipalName principalName : principalNameList) {
|
||||
principals.add(principalName.getName());
|
||||
}
|
||||
|
||||
assertThat(principals).containsExactlyInAnyOrder("foo/bar@" + kdc.getRealm(), "bar/foo@" + kdc.getRealm());
|
||||
|
||||
}
|
||||
|
||||
private static final class KerberosConfiguration extends Configuration {
|
||||
|
||||
private String principal;
|
||||
|
||||
private String keytab;
|
||||
|
||||
private boolean isInitiator;
|
||||
|
||||
private KerberosConfiguration(String principal, File keytab, boolean client) {
|
||||
this.principal = principal;
|
||||
this.keytab = keytab.getAbsolutePath();
|
||||
this.isInitiator = client;
|
||||
}
|
||||
|
||||
private static Configuration createClientConfig(String principal, File keytab) {
|
||||
return new KerberosConfiguration(principal, keytab, true);
|
||||
}
|
||||
|
||||
private static Configuration createServerConfig(String principal, File keytab) {
|
||||
return new KerberosConfiguration(principal, keytab, false);
|
||||
}
|
||||
|
||||
private static String getKrb5LoginModuleName() {
|
||||
return System.getProperty("java.vendor").contains("IBM") ? "com.ibm.security.auth.module.Krb5LoginModule"
|
||||
: "com.sun.security.auth.module.Krb5LoginModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
|
||||
Map<String, String> options = new HashMap<String, String>();
|
||||
options.put("principal", this.principal);
|
||||
options.put("refreshKrb5Config", "true");
|
||||
if (IBM_JAVA) {
|
||||
options.put("useKeytab", this.keytab);
|
||||
options.put("credsType", "both");
|
||||
}
|
||||
else {
|
||||
options.put("keyTab", this.keytab);
|
||||
options.put("useKeyTab", "true");
|
||||
options.put("storeKey", "true");
|
||||
options.put("doNotPrompt", "true");
|
||||
options.put("useTicketCache", "true");
|
||||
options.put("renewTGT", "true");
|
||||
options.put("isInitiator", Boolean.toString(this.isInitiator));
|
||||
}
|
||||
String ticketCache = System.getenv("KRB5CCNAME");
|
||||
if (ticketCache != null) {
|
||||
options.put("ticketCache", ticketCache);
|
||||
}
|
||||
options.put("debug", "true");
|
||||
|
||||
return new AppConfigurationEntry[] { new AppConfigurationEntry(getKrb5LoginModuleName(),
|
||||
AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
'}'
|
|
@ -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
|
|
@ -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'
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.web.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
|
||||
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Parses the SPNEGO authentication Header, which was generated by the browser and creates
|
||||
* a {@link KerberosServiceRequestToken} out if it. It will then call the
|
||||
* {@link AuthenticationManager}.
|
||||
*
|
||||
* <p>
|
||||
* A typical Spring Security configuration might look like this:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* <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>
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* If you get a "GSSException: Channel binding mismatch (Mechanism level:ChannelBinding
|
||||
* not provided!) have a look at this
|
||||
* <a href="https://bugs.sun.com/view_bug.do?bug_id=6851973">bug</a>.
|
||||
* </p>
|
||||
* <p>
|
||||
* A workaround unti this is fixed in the JVM is to change
|
||||
* </p>
|
||||
* HKEY_LOCAL_MACHINE\System \CurrentControlSet\Control\LSA\SuppressExtendedProtection to
|
||||
* 0x02
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Jeremy Stone
|
||||
* @author Denis Angilella
|
||||
* @since 1.0
|
||||
* @see KerberosServiceAuthenticationProvider
|
||||
* @see SpnegoEntryPoint
|
||||
*/
|
||||
public class SpnegoAuthenticationProcessingFilter extends OncePerRequestFilter {
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
|
||||
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
|
||||
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
private AuthenticationSuccessHandler successHandler;
|
||||
|
||||
private AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
|
||||
|
||||
private boolean skipIfAlreadyAuthenticated = true;
|
||||
|
||||
private boolean stopFilterChainOnSuccessfulAuthentication = false;
|
||||
|
||||
/**
|
||||
* Authentication header prefix sent by IE/Windows when the domain controller fails to
|
||||
* issue a Kerberos ticket for the URL.
|
||||
*
|
||||
* "TlRMTVNTUA" is the base64 encoding of "NTLMSSP". This will be followed by the
|
||||
* actual token.
|
||||
**/
|
||||
private static final String NTLMSSP_PREFIX = "Negotiate TlRMTVNTUA";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
if (this.skipIfAlreadyAuthenticated) {
|
||||
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (existingAuth != null && existingAuth.isAuthenticated()
|
||||
&& !(existingAuth instanceof AnonymousAuthenticationToken)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String header = request.getHeader("Authorization");
|
||||
|
||||
if (header != null && ((header.startsWith("Negotiate ") && !header.startsWith(NTLMSSP_PREFIX))
|
||||
|| header.startsWith("Kerberos "))) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
|
||||
}
|
||||
byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8");
|
||||
byte[] kerberosTicket = Base64.getDecoder().decode(base64Token);
|
||||
KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket);
|
||||
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
Authentication authentication;
|
||||
try {
|
||||
authentication = this.authenticationManager.authenticate(authenticationRequest);
|
||||
}
|
||||
catch (AuthenticationException ex) {
|
||||
// That shouldn't happen, as it is most likely a wrong
|
||||
// configuration on the server side
|
||||
this.logger.warn("Negotiate Header was invalid: " + header, ex);
|
||||
this.securityContextHolderStrategy.clearContext();
|
||||
if (this.failureHandler != null) {
|
||||
this.failureHandler.onAuthenticationFailure(request, response, ex);
|
||||
}
|
||||
else {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.flushBuffer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.sessionStrategy.onAuthentication(authentication, request, response);
|
||||
|
||||
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
|
||||
context.setAuthentication(authentication);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
if (this.successHandler != null) {
|
||||
this.successHandler.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
if (this.stopFilterChainOnSuccessfulAuthentication) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws ServletException {
|
||||
super.afterPropertiesSet();
|
||||
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
|
||||
}
|
||||
|
||||
/**
|
||||
* The authentication manager for validating the ticket.
|
||||
* @param authenticationManager the authentication manager
|
||||
*/
|
||||
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* This handler is called after a successful authentication. One can add additional
|
||||
* authentication behavior by setting this.
|
||||
* </p>
|
||||
* <p>
|
||||
* Default is null, which means nothing additional happens
|
||||
* </p>
|
||||
* @param successHandler the authentication success handler
|
||||
*/
|
||||
public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
|
||||
this.successHandler = successHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* This handler is called after a failure authentication. In most cases you only get
|
||||
* Kerberos/SPNEGO failures with a wrong server or network configurations and not
|
||||
* during runtime. If the client encounters an error, he will just stop the
|
||||
* communication with server and therefore this handler will not be called in this
|
||||
* case.
|
||||
* </p>
|
||||
* <p>
|
||||
* Default is null, which means that the Filter returns the HTTP 500 code
|
||||
* </p>
|
||||
* @param failureHandler the authentication failure handler
|
||||
*/
|
||||
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
|
||||
this.failureHandler = failureHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should Kerberos authentication be skipped if a user is already authenticated for
|
||||
* this request (e.g. in the HTTP session).
|
||||
* @param skipIfAlreadyAuthenticated default is true
|
||||
*/
|
||||
public void setSkipIfAlreadyAuthenticated(boolean skipIfAlreadyAuthenticated) {
|
||||
this.skipIfAlreadyAuthenticated = skipIfAlreadyAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* The session handling strategy which will be invoked immediately after an
|
||||
* authentication request is successfully processed by the
|
||||
* <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
|
||||
* session identifier to prevent session fixation attacks.
|
||||
* @param sessionStrategy the implementation to use. If not set a null implementation
|
||||
* is used.
|
||||
*/
|
||||
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
|
||||
this.sessionStrategy = sessionStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication details source.
|
||||
* @param authenticationDetailsSource the authentication details source
|
||||
*/
|
||||
public void setAuthenticationDetailsSource(
|
||||
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
|
||||
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
|
||||
this.authenticationDetailsSource = authenticationDetailsSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@code false} (the default) and authentication is successful, the request
|
||||
* will be processed by the next filter in the chain. If {@code true} and
|
||||
* authentication is successful, the filter chain will stop here.
|
||||
* @param shouldStop set to {@code true} to prevent the next filter in the chain from
|
||||
* processing the request after a successful authentication.
|
||||
* @since 1.0.2
|
||||
*/
|
||||
public void setStopFilterChainOnSuccessfulAuthentication(boolean shouldStop) {
|
||||
this.stopFilterChainOnSuccessfulAuthentication = shouldStop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
|
||||
* authentication success. The default action is not to save the
|
||||
* {@link SecurityContext}.
|
||||
* @param securityContextRepository the {@link SecurityContextRepository} to use.
|
||||
* Cannot be null.
|
||||
*/
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
|
||||
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
|
||||
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
|
||||
* use. Cannot be null.
|
||||
*/
|
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
||||
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.web.authentication;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Sends back a request for a Negotiate Authentication to the browser.
|
||||
*
|
||||
* <p>
|
||||
* With optional configured <code>forwardUrl</code> it is possible to use form login as
|
||||
* fallback authentication.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This approach enables security configuration to use SPNEGO in combination with login
|
||||
* form as fallback for clients that do not support this kind of authentication. Set
|
||||
* Response Code 401 - unauthorized and forward to login page. A useful scenario might be
|
||||
* an environment where windows domain is present but it is required to access the
|
||||
* application also from non domain client devices. One could use a combination with form
|
||||
* based LDAP login.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* See <code>spnego-with-form-login.xml</code> in spring-security-kerberos-sample for
|
||||
* details
|
||||
* </p>
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Andre Schaefer, Namics AG
|
||||
* @since 1.0
|
||||
* @see SpnegoAuthenticationProcessingFilter
|
||||
*/
|
||||
public class SpnegoEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(SpnegoEntryPoint.class);
|
||||
|
||||
private final String forwardUrl;
|
||||
|
||||
private final HttpMethod forwardMethod;
|
||||
|
||||
private final boolean forward;
|
||||
|
||||
/**
|
||||
* Instantiates a new spnego entry point. Using this constructor the EntryPoint will
|
||||
* Sends back a request for a Negotiate Authentication to the browser without
|
||||
* providing a fallback mechanism for login, Use constructor with forwardUrl to
|
||||
* provide form based login.
|
||||
*/
|
||||
public SpnegoEntryPoint() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new spnego entry point. This constructor enables security
|
||||
* configuration to use SPNEGO in combination with a fallback page (login form, custom
|
||||
* 401 page ...). The forward method will be the same as the original request.
|
||||
* @param forwardUrl URL where the login page can be found. Should be relative to the
|
||||
* web-app context path (include a leading {@code /}) and can't be absolute URL.
|
||||
*/
|
||||
public SpnegoEntryPoint(String forwardUrl) {
|
||||
this(forwardUrl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new spnego entry point. This constructor enables security
|
||||
* configuration to use SPNEGO in combination a fallback page (login form, custom 401
|
||||
* page ...). The forward URL will be accessed via provided HTTP method.
|
||||
* @param forwardUrl URL where the login page can be found. Should be relative to the
|
||||
* web-app context path (include a leading {@code /}) and can't be absolute URL.
|
||||
* @param forwardMethod HTTP method to use when accessing the forward URL
|
||||
*/
|
||||
public SpnegoEntryPoint(String forwardUrl, HttpMethod forwardMethod) {
|
||||
if (StringUtils.hasText(forwardUrl)) {
|
||||
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), "Forward url specified must be a valid forward URL");
|
||||
Assert.isTrue(!UrlUtils.isAbsoluteUrl(forwardUrl), "Forward url specified must not be absolute");
|
||||
|
||||
this.forwardUrl = forwardUrl;
|
||||
this.forwardMethod = forwardMethod;
|
||||
this.forward = true;
|
||||
}
|
||||
else {
|
||||
this.forwardUrl = null;
|
||||
this.forwardMethod = null;
|
||||
this.forward = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
|
||||
throws IOException, ServletException {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Add header WWW-Authenticate:Negotiate to " + request.getRequestURL() + ", forward: "
|
||||
+ (this.forward ? this.forwardUrl : "no"));
|
||||
}
|
||||
response.addHeader("WWW-Authenticate", "Negotiate");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
|
||||
if (this.forward) {
|
||||
RequestDispatcher dispatcher = request.getRequestDispatcher(this.forwardUrl);
|
||||
HttpServletRequest fwdRequest = (this.forwardMethod != null) ? new HttpServletRequestWrapper(request) {
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return SpnegoEntryPoint.this.forwardMethod.name();
|
||||
}
|
||||
} : request;
|
||||
dispatcher.forward(fwdRequest, response);
|
||||
}
|
||||
else {
|
||||
response.flushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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[]
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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[]
|
|
@ -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[]
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.kerberos.web;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Test class for {@link SpnegoEntryPoint}
|
||||
*
|
||||
* @author Mike Wiesner
|
||||
* @author Janne Valkealahti
|
||||
* @author Andre Schaefer, Namics AG
|
||||
* @since 1.0
|
||||
*/
|
||||
public class SpnegoEntryPointTests {
|
||||
|
||||
private SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
|
||||
|
||||
@Test
|
||||
public void testEntryPointOk() throws Exception {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
this.entryPoint.commence(request, response, null);
|
||||
|
||||
verify(response).addHeader("WWW-Authenticate", "Negotiate");
|
||||
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryPointOkWithDispatcher() throws Exception {
|
||||
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint();
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
|
||||
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
|
||||
entryPoint.commence(request, response, null);
|
||||
verify(response).addHeader("WWW-Authenticate", "Negotiate");
|
||||
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryPointForwardOk() throws Exception {
|
||||
String forwardUrl = "/login";
|
||||
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
|
||||
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
|
||||
entryPoint.commence(request, response, null);
|
||||
verify(response).addHeader("WWW-Authenticate", "Negotiate");
|
||||
verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(request).getRequestDispatcher(forwardUrl);
|
||||
verify(requestDispatcher).forward(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForwardUsesDefaultHttpMethod() throws Exception {
|
||||
ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
|
||||
String forwardUrl = "/login";
|
||||
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
given(request.getMethod()).willReturn(RequestMethod.POST.name());
|
||||
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
|
||||
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
|
||||
entryPoint.commence(request, response, null);
|
||||
verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
|
||||
assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.POST.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForwardUsesCustomHttpMethod() throws Exception {
|
||||
ArgumentCaptor<HttpServletRequest> servletRequestCaptor = ArgumentCaptor.forClass(HttpServletRequest.class);
|
||||
String forwardUrl = "/login";
|
||||
SpnegoEntryPoint entryPoint = new SpnegoEntryPoint(forwardUrl, HttpMethod.DELETE);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
RequestDispatcher requestDispatcher = mock(RequestDispatcher.class);
|
||||
given(request.getRequestDispatcher(anyString())).willReturn(requestDispatcher);
|
||||
entryPoint.commence(request, response, null);
|
||||
verify(requestDispatcher).forward(servletRequestCaptor.capture(), eq(response));
|
||||
assertThat(servletRequestCaptor.getValue().getMethod()).isEqualTo(HttpMethod.DELETE.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEntryPointForwardAbsolute() throws Exception {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new SpnegoEntryPoint("http://test/login"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- tag::snippetA[] -->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:sec="http://www.springframework.org/schema/security"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.2.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.2.xsd
|
||||
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
|
||||
|
||||
<sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true">
|
||||
<sec:intercept-url pattern="/" access="permitAll" />
|
||||
<sec:intercept-url pattern="/home" access="permitAll" />
|
||||
<sec:intercept-url pattern="/**" access="authenticated"/>
|
||||
</sec:http>
|
||||
|
||||
<sec:authentication-manager alias="authenticationManager">
|
||||
<sec:authentication-provider ref="kerberosAuthenticationProvider"/>
|
||||
</sec:authentication-manager>
|
||||
|
||||
<bean id="kerberosAuthenticationProvider"
|
||||
class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
|
||||
<property name="kerberosClient">
|
||||
<bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
|
||||
<property name="debug" value="true"/>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="userDetailsService" ref="dummyUserDetailsService"/>
|
||||
</bean>
|
||||
|
||||
<bean
|
||||
class="org.springframework.security.kerberos.authentication.sun.GlobalSunJaasKerberosConfig">
|
||||
<property name="debug" value="true" />
|
||||
<property name="krbConfLocation" value="/path/to/krb5.ini"/>
|
||||
</bean>
|
||||
|
||||
<bean id="dummyUserDetailsService"
|
||||
class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
|
||||
|
||||
<bean id="spnegoEntryPoint"
|
||||
class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
|
||||
<constructor-arg value="/login" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
<!-- end::snippetA[] -->
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- tag::snippetA[] -->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:sec="http://www.springframework.org/schema/security"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-4.1.xsd
|
||||
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd">
|
||||
|
||||
<sec:http entry-point-ref="spnegoEntryPoint" use-expressions="true" >
|
||||
<sec:intercept-url pattern="/" access="permitAll" />
|
||||
<sec:intercept-url pattern="/home" access="permitAll" />
|
||||
<sec:intercept-url pattern="/login" access="permitAll" />
|
||||
<sec:intercept-url pattern="/**" access="authenticated"/>
|
||||
<sec:form-login login-page="/login" />
|
||||
<sec:custom-filter ref="spnegoAuthenticationProcessingFilter"
|
||||
before="BASIC_AUTH_FILTER" />
|
||||
</sec:http>
|
||||
|
||||
<sec:authentication-manager alias="authenticationManager">
|
||||
<sec:authentication-provider ref="kerberosAuthenticationProvider" />
|
||||
<sec:authentication-provider ref="kerberosServiceAuthenticationProvider" />
|
||||
</sec:authentication-manager>
|
||||
|
||||
<bean id="kerberosAuthenticationProvider"
|
||||
class="org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider">
|
||||
<property name="userDetailsService" ref="dummyUserDetailsService"/>
|
||||
<property name="kerberosClient">
|
||||
<bean class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient">
|
||||
<property name="debug" value="true"/>
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="spnegoEntryPoint"
|
||||
class="org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint" >
|
||||
<constructor-arg value="/login" />
|
||||
</bean>
|
||||
|
||||
<bean id="spnegoAuthenticationProcessingFilter"
|
||||
class="org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter">
|
||||
<property name="authenticationManager" ref="authenticationManager" />
|
||||
</bean>
|
||||
|
||||
<bean id="kerberosServiceAuthenticationProvider"
|
||||
class="org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider">
|
||||
<property name="ticketValidator">
|
||||
<bean
|
||||
class="org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator">
|
||||
<property name="servicePrincipal" value="${app.service-principal}" />
|
||||
<property name="keyTabLocation" value="${app.keytab-location}" />
|
||||
<property name="debug" value="true" />
|
||||
</bean>
|
||||
</property>
|
||||
<property name="userDetailsService" ref="dummyUserDetailsService" />
|
||||
</bean>
|
||||
|
||||
<bean id="dummyUserDetailsService"
|
||||
class="org.springframework.security.kerberos.docs.DummyUserDetailsService" />
|
||||
|
||||
</beans>
|
||||
<!-- end::snippetA[] -->
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.1.xsd
|
||||
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util-4.1.xsd">
|
||||
|
||||
<context:property-placeholder location="app.properties"/>
|
||||
|
||||
</beans>
|