diff --git a/build.gradle b/build.gradle index 341527c..fe9f499 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,9 @@ configure(subprojects) { subproject -> } dependencies { + testCompile "org.mockito:mockito-core:$mockitoVersion" testCompile "junit:junit:$junitVersion" +// testRuntime("log4j:log4j:$log4jVersion") } } @@ -93,11 +95,53 @@ project('spring-security-kerberos-core') { compile "org.springframework:spring-tx:$springVersion" compile "org.springframework:spring-jdbc:$springVersion" compile "org.springframework:spring-web:$springVersion" - compile "org.springframework.security:spring-security-core:$springSecurityVersion" + compile "org.springframework.security:spring-security-config:$springSecurityVersion" compile "org.springframework.security:spring-security-web:$springSecurityVersion" - compile("commons-logging:commons-logging:1.1.1", optional) - compile("javax.servlet:servlet-api:2.5", provided) - testCompile "org.mockito:mockito-core:1.9.0" + compile("javax.servlet:javax.servlet-api:$servletApi3Version", optional) + } +} + +project('spring-security-kerberos-client') { + description = 'Spring Security Kerberos Client' + + configurations { + all*.exclude group: "org.apache.directory.api", module: "api-ldap-schema-data" + } + + dependencies { + compile project(":spring-security-kerberos-core") + compile "org.springframework:spring-web:$springVersion" + compile "org.apache.httpcomponents:httpclient:$httpclientVersion" + testCompile project(":spring-security-kerberos-test") + testCompile "org.springframework.boot:spring-boot-autoconfigure:$springBootVersion" + testRuntime "org.apache.tomcat.embed:tomcat-embed-core:$tomcatEmbedVersion" + testRuntime "org.apache.tomcat.embed:tomcat-embed-logging-juli:$tomcatEmbedVersion" + testRuntime "org.springframework:spring-webmvc:$springVersion" + } +} + +project('spring-security-kerberos-test') { + description = 'Spring Security Kerberos Test' + + configurations { + all*.exclude group: "org.apache.directory.api", module: "api-ldap-schema-data" + } + + dependencies { + compile "junit:junit:$junitVersion" + compile "org.apache.directory.server:apacheds-core-api:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-interceptor-kerberos:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-protocol-shared:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-protocol-kerberos:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-ldif-partition:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-mavibot-partition:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-jdbm-partition:$apacheDirServerVersion" + compile "org.apache.directory.server:apacheds-protocol-ldap:$apacheDirServerVersion" + compile("org.apache.directory.api:api-all:$apacheDirApiVersion") { + exclude group: "xml-apis", module: "xml-apis" + exclude group: "xpp3", module: "xpp3" + exclude group: "dom4j", module: "dom4j" + } } } @@ -114,9 +158,9 @@ project('spring-security-kerberos-sample') { compile "org.springframework:spring-jdbc:$springVersion" compile "org.springframework:spring-web:$springVersion" compile project(":spring-security-kerberos-core") - compile("javax.servlet:servlet-api:2.5", provided) + compile("javax.servlet:servlet-api:$servletApi2Version", provided) compile("org.springframework.security:spring-security-config:$springSecurityVersion") - compile('log4j:log4j:1.2.17') + compile("log4j:log4j:$log4jVersion") } } diff --git a/gradle.properties b/gradle.properties index 7e757a8..a67c042 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,19 @@ ## Common libraries +log4jVersion = 1.2.17 springVersion = 4.1.4.RELEASE +springBootVersion = 1.2.1.RELEASE springSecurityVersion = 3.2.5.RELEASE +httpclientVersion = 4.3.3 +apacheDirServerVersion = 2.0.0-M15 +apacheDirApiVersion = 1.0.0-M20 + +servletApi2Version = 2.5 +servletApi3Version = 3.1.0 ## Common testing libraries junitVersion = 4.11 +tomcatEmbedVersion = 7.0.56 +mockitoVersion = 1.9.5 +hamcrestVersion = 1.3 version=1.0.0.BUILD-SNAPSHOT diff --git a/settings.gradle b/settings.gradle index 972f69d..e32ec3e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'spring-security-kerberos' include 'spring-security-kerberos-core' -include 'spring-security-kerberos-sample' \ No newline at end of file +include 'spring-security-kerberos-client' +include 'spring-security-kerberos-test' +include 'spring-security-kerberos-sample' diff --git a/spring-security-kerberos-client/src/main/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplate.java b/spring-security-kerberos-client/src/main/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplate.java new file mode 100644 index 0000000..8750de1 --- /dev/null +++ b/spring-security-kerberos-client/src/main/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplate.java @@ -0,0 +1,162 @@ +/* + * 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.extensions.kerberos.client; + +import java.net.URI; +import java.security.Principal; +import java.security.PrivilegedAction; +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 org.apache.http.auth.AuthSchemeProvider; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.AuthSchemes; +import org.apache.http.config.Lookup; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.impl.auth.SPNegoSchemeFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +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. + * + * @author Janne Valkealahti + * + */ +public class KerberosRestTemplate extends RestTemplate { + + private static final Credentials credentials = new NullCredentials(); + + private final String keyTabLocation; + private final String servicePrincipalName; + + public KerberosRestTemplate(String keyTabLocation, String servicePrincipalName) { + this(keyTabLocation, servicePrincipalName, buildHttpClient()); + } + + public KerberosRestTemplate(String keyTabLocation, String servicePrincipalName, HttpClient httpClient) { + super(new HttpComponentsClientHttpRequestFactory(httpClient)); + this.keyTabLocation = keyTabLocation; + this.servicePrincipalName = servicePrincipalName; + } + + private static HttpClient buildHttpClient() { + HttpClientBuilder builder = HttpClientBuilder.create(); + Lookup authSchemeRegistry = RegistryBuilder. create() + .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true)).build(); + builder.setDefaultAuthSchemeRegistry(authSchemeRegistry); + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(null, -1, null), credentials); + builder.setDefaultCredentialsProvider(credentialsProvider); + CloseableHttpClient httpClient = builder.build(); + return httpClient; + } + + @Override + protected final T doExecute(final URI url, final HttpMethod method, final RequestCallback requestCallback, + final ResponseExtractor responseExtractor) throws RestClientException { + + try { + ClientLoginConfig loginConfig = new ClientLoginConfig(keyTabLocation, servicePrincipalName, true); + Set princ = new HashSet(1); + princ.add(new KerberosPrincipal(servicePrincipalName)); + Subject sub = new Subject(false, princ, new HashSet(), new HashSet()); + LoginContext lc = new LoginContext("", sub, null, loginConfig); + lc.login(); + Subject serviceSubject = lc.getSubject(); + return Subject.doAs(serviceSubject, new PrivilegedAction() { + + @Override + public T run() { + return KerberosRestTemplate.this.doExecuteSubject(url, method, requestCallback, responseExtractor); + } + }); + + } catch (Exception e) { + throw new RestClientException("Error running rest call", e); + } + } + + private T doExecuteSubject(URI url, HttpMethod method, RequestCallback requestCallback, + ResponseExtractor responseExtractor) throws RestClientException { + return super.doExecute(url, method, requestCallback, responseExtractor); + } + + private static class ClientLoginConfig extends Configuration { + + private final String keyTabLocation; + private final String servicePrincipalName; + private final boolean debug; + + public ClientLoginConfig(String keyTabLocation, String servicePrincipalName, boolean debug) { + super(); + this.keyTabLocation = keyTabLocation; + this.servicePrincipalName = servicePrincipalName; + this.debug = debug; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + HashMap options = new HashMap(); + options.put("useKeyTab", "true"); + options.put("keyTab", this.keyTabLocation); + options.put("principal", this.servicePrincipalName); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + if (this.debug) { + options.put("debug", "true"); + } + options.put("isInitiator", "true"); + + 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 String getPassword() { + return null; + } + + } + +} diff --git a/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplateTests.java b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplateTests.java new file mode 100644 index 0000000..3b4f164 --- /dev/null +++ b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/KerberosRestTemplateTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.extensions.kerberos.client; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.extensions.kerberos.test.KerberosSecurityTestcase; +import org.springframework.security.extensions.kerberos.test.MiniKdc; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +public class KerberosRestTemplateTests extends KerberosSecurityTestcase { + + private ConfigurableApplicationContext context; + + @After + public void close() { + if (context != null) { + context.close(); + } + context = null; + } + + @Test + public void testSpnego() throws Exception { + + MiniKdc kdc = getKdc(); + File workDir = getWorkDir(); + + String serverPrincipal = "HTTP/localhost"; + File serverKeytab = new File(workDir, "server.keytab"); + kdc.createPrincipal(serverKeytab, serverPrincipal); + + String clientPrincipal = "client/localhost"; + File clientKeytab = new File(workDir, "client.keytab"); + kdc.createPrincipal(clientKeytab, clientPrincipal); + + + context = SpringApplication.run(new Object[] { WebSecurityConfig.class, VanillaWebConfiguration.class, + WebConfiguration.class }, new String[] { "--security.basic.enabled=true", + "--security.user.name=username", "--security.user.password=password", + "--serverPrincipal=" + serverPrincipal, "--serverKeytab=" + serverKeytab.getAbsolutePath() }); + + PortInitListener portInitListener = context.getBean(PortInitListener.class); + assertThat(portInitListener.latch.await(10, TimeUnit.SECONDS), is(true)); + int port = portInitListener.port; + + KerberosRestTemplate restTemplate = new KerberosRestTemplate(clientKeytab.getAbsolutePath(), clientPrincipal); + + String response = restTemplate.getForObject("http://localhost:" + port + "/hello", String.class); + assertThat(response, is("home")); + } + + protected static class PortInitListener implements ApplicationListener { + + public int port; + public CountDownLatch latch = new CountDownLatch(1); + + @Override + public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) { + port = event.getEmbeddedServletContainer().getPort(); + latch.countDown(); + } + + } + + @Configuration + protected static class VanillaWebConfiguration { + + @Bean + public PortInitListener portListener() { + return new PortInitListener(); + } + + @Bean + public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { + TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory(); + factory.setPort(0); + return factory; + } + } + + @MinimalWebConfiguration + @Import(SecurityAutoConfiguration.class) + @Controller + protected static class WebConfiguration { + + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public String home() { + return "home"; + } + + } + + @Configuration + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Import({ EmbeddedServletContainerAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + protected static @interface MinimalWebConfiguration { + } + +} diff --git a/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfig.java b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfig.java new file mode 100644 index 0000000..e956d57 --- /dev/null +++ b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfig.java @@ -0,0 +1,110 @@ +/* + * 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.extensions.kerberos.client; + +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.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; +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 org.springframework.security.extensions.kerberos.KerberosServiceAuthenticationProvider; +import org.springframework.security.extensions.kerberos.SunJaasKerberosTicketValidator; +import org.springframework.security.extensions.kerberos.web.SpnegoAuthenticationProcessingFilter; +import org.springframework.security.extensions.kerberos.web.SpnegoEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +@Configuration +@EnableWebMvcSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${serverPrincipal}") + private String serverPrincipal; + + @Value("${serverKeytab}") + private String serverKeytab; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .exceptionHandling().authenticationEntryPoint(spnegoEntryPoint()).and() + .authorizeRequests() + .antMatchers("/", "/home").permitAll() + .antMatchers("/hello").access("hasRole('ROLE_USER')") + .anyRequest().authenticated() + .and() + + .addFilterBefore(spnegoAuthenticationProcessingFilter(authenticationManagerBean()), BasicAuthenticationFilter.class); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(kerberosServiceAuthenticationProvider()); + } + + @Bean + public SpnegoEntryPoint spnegoEntryPoint() { + return new SpnegoEntryPoint(); + } + + @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(serverPrincipal); + ticketValidator.setKeyTabLocation(new FileSystemResource(serverKeytab)); + ticketValidator.setDebug(true); + return ticketValidator; + } + + @Bean + public DummyUserDetailsService dummyUserDetailsService() { + return new DummyUserDetailsService(); + } + + static class DummyUserDetailsService implements UserDetailsService { + + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return new User(username, "notUsed", true, true, true, true, + AuthorityUtils.createAuthorityList("ROLE_USER")); + } + + } + +} diff --git a/spring-security-kerberos-client/src/test/resources/log4j.properties b/spring-security-kerberos-client/src/test/resources/log4j.properties new file mode 100644 index 0000000..42ac2a2 --- /dev/null +++ b/spring-security-kerberos-client/src/test/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n + +log4j.category.org.springframework.boot=INFO +xlog4j.category.org.apache.http.wire=TRACE +xlog4j.category.org.apache.http.headers=TRACE + diff --git a/spring-security-kerberos-client/src/test/resources/minikdc-krb5.conf b/spring-security-kerberos-client/src/test/resources/minikdc-krb5.conf new file mode 100644 index 0000000..d118dd1 --- /dev/null +++ b/spring-security-kerberos-client/src/test/resources/minikdc-krb5.conf @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# 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. +# +[libdefaults] + default_realm = {0} + udp_preference_limit = 1 + +[realms] + {0} = '{' + kdc = {1}:{2} + '}' \ No newline at end of file diff --git a/spring-security-kerberos-client/src/test/resources/minikdc.ldiff b/spring-security-kerberos-client/src/test/resources/minikdc.ldiff new file mode 100644 index 0000000..603ccb5 --- /dev/null +++ b/spring-security-kerberos-client/src/test/resources/minikdc.ldiff @@ -0,0 +1,47 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# 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. +# +dn: ou=users,dc=${0},dc=${1} +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=krbtgt,ou=users,dc=${0},dc=${1} +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: KDC Service +sn: Service +uid: krbtgt +userPassword: secret +krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3} +krb5KeyVersionNumber: 0 + +dn: uid=ldap,ou=users,dc=${0},dc=${1} +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: LDAP +sn: Service +uid: ldap +userPassword: secret +krb5PrincipalName: ldap/${4}@${2}.${3} +krb5KeyVersionNumber: 0 \ No newline at end of file diff --git a/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/KerberosSecurityTestcase.java b/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/KerberosSecurityTestcase.java new file mode 100644 index 0000000..d994a9a --- /dev/null +++ b/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/KerberosSecurityTestcase.java @@ -0,0 +1,83 @@ +/* + * 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.extensions.kerberos.test; + +import org.junit.After; +import org.junit.Before; + +import java.io.File; +import java.util.Properties; + +/** + * 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). + * + * @author Original Hadoop MiniKdc Authors + * @author Janne Valkealahti + * + */ +public class KerberosSecurityTestcase { + private MiniKdc kdc; + private File workDir; + private Properties conf; + + @Before + public void startMiniKdc() throws Exception { + createTestDir(); + createMiniKdcConf(); + + kdc = new MiniKdc(conf, workDir); + 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() { + workDir = new File(System.getProperty("test.dir", "target")); + } + + /** + * Create a Kdc configuration + */ + public void createMiniKdcConf() { + conf = MiniKdc.createConf(); + } + + @After + public void stopMiniKdc() { + if (kdc != null) { + kdc.stop(); + } + } + + public MiniKdc getKdc() { + return kdc; + } + + public File getWorkDir() { + return workDir; + } + + public Properties getConf() { + return conf; + } + +} diff --git a/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/MiniKdc.java b/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/MiniKdc.java new file mode 100644 index 0000000..999afda --- /dev/null +++ b/spring-security-kerberos-test/src/main/java/org/springframework/security/extensions/kerberos/test/MiniKdc.java @@ -0,0 +1,546 @@ +/* + * 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.extensions.kerberos.test; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.text.StrSubstitutor; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor; +import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader; +import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager; +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.api.CacheService; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.InstanceLayout; +import org.apache.directory.server.core.api.schema.SchemaPartition; +import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition; +import org.apache.directory.server.core.partition.ldif.LdifPartition; +import org.apache.directory.server.kerberos.kdc.KdcServer; +import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory; +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.protocol.shared.transport.UdpTransport; +import org.apache.directory.server.xdbm.Index; +import org.apache.directory.shared.kerberos.KerberosTime; +import org.apache.directory.shared.kerberos.codec.types.EncryptionType; +import org.apache.directory.shared.kerberos.components.EncryptionKey; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +/** + * Mini KDC based on Apache Directory Server that can be embedded in testcases + * or used from command line as a standalone KDC. + *

+ * From within testcases: + *

+ * MiniKdc sets 2 System properties when started and un-sets them when stopped: + *

    + *
  • java.security.krb5.conf: set to the MiniKDC real/host/port
  • + *
  • sun.security.krb5.debug: set to the debug value provided in the + * configuration
  • + *
+ * Because of this, multiple MiniKdc instances cannot be started in parallel. + * For example, running testcases in parallel that start a KDC each. To + * accomplish this a single MiniKdc should be used for all testcases running in + * parallel. + *

+ * MiniKdc default configuration values are: + *

    + *
  • org.name=EXAMPLE (used to create the REALM)
  • + *
  • org.domain=COM (used to create the REALM)
  • + *
  • kdc.bind.address=localhost
  • + *
  • kdc.port=0 (ephemeral port)
  • + *
  • instance=DefaultKrbServer
  • + *
  • max.ticket.lifetime=86400000 (1 day)
  • + *
  • max.renewable.lifetime=604800000 (7 days)
  • + *
  • transport=TCP
  • + *
  • debug=false
  • + *
+ * The generated krb5.conf forces TCP connections. + *

+ * + * @author Original Hadoop MiniKdc Authors + * @author Janne Valkealahti + * + */ +public class MiniKdc { + + public static void main(String[] args) throws Exception { + if (args.length < 4) { + System.out.println("Arguments: " + " []+"); + System.exit(1); + } + File workDir = new File(args[0]); + if (!workDir.exists()) { + throw new RuntimeException("Specified work directory does not exists: " + workDir.getAbsolutePath()); + } + Properties conf = createConf(); + File file = new File(args[1]); + if (!file.exists()) { + throw new RuntimeException("Specified configuration does not exists: " + file.getAbsolutePath()); + } + Properties userConf = new Properties(); + InputStreamReader r = null; + try { + r = new InputStreamReader(new FileInputStream(file), Charsets.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.getHost()); + System.out.println(" krb5conf : " + krb5conf); + System.out.println(); + System.out.println(" created keytab : " + keytabFile); + System.out.println(" with principals : " + Arrays.asList(principals)); + System.out.println(); + System.out.println(" Do or kill to stop it"); + System.out.println("---------------------------------------------------"); + System.out.println(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + miniKdc.stop(); + } + }); + } else { + throw new RuntimeException("Cannot rename KDC's krb5conf to " + krb5conf.getAbsolutePath()); + } + } + + private static final Logger LOG = LoggerFactory.getLogger(MiniKdc.class); + + public static final String ORG_NAME = "org.name"; + public static final String ORG_DOMAIN = "org.domain"; + public static final String KDC_BIND_ADDRESS = "kdc.bind.address"; + public static final String KDC_PORT = "kdc.port"; + public static final String INSTANCE = "instance"; + public static final String MAX_TICKET_LIFETIME = "max.ticket.lifetime"; + public static final String MAX_RENEWABLE_LIFETIME = "max.renewable.lifetime"; + public static final String TRANSPORT = "transport"; + public static final String DEBUG = "debug"; + + private static final Set PROPERTIES = new HashSet(); + private static final Properties DEFAULT_CONFIG = new Properties(); + + static { + PROPERTIES.add(ORG_NAME); + PROPERTIES.add(ORG_DOMAIN); + PROPERTIES.add(KDC_BIND_ADDRESS); + PROPERTIES.add(KDC_BIND_ADDRESS); + PROPERTIES.add(KDC_PORT); + PROPERTIES.add(INSTANCE); + PROPERTIES.add(TRANSPORT); + PROPERTIES.add(MAX_TICKET_LIFETIME); + PROPERTIES.add(MAX_RENEWABLE_LIFETIME); + + DEFAULT_CONFIG.setProperty(KDC_BIND_ADDRESS, "localhost"); + DEFAULT_CONFIG.setProperty(KDC_PORT, "0"); + DEFAULT_CONFIG.setProperty(INSTANCE, "DefaultKrbServer"); + DEFAULT_CONFIG.setProperty(ORG_NAME, "EXAMPLE"); + DEFAULT_CONFIG.setProperty(ORG_DOMAIN, "COM"); + DEFAULT_CONFIG.setProperty(TRANSPORT, "TCP"); + DEFAULT_CONFIG.setProperty(MAX_TICKET_LIFETIME, "86400000"); + DEFAULT_CONFIG.setProperty(MAX_RENEWABLE_LIFETIME, "604800000"); + DEFAULT_CONFIG.setProperty(DEBUG, "false"); + } + + /** + * Convenience method that returns MiniKdc default configuration. + *

+ * The returned configuration is a copy, it can be customized before using + * it to create a MiniKdc. + * + * @return a MiniKdc default configuration. + */ + public static Properties createConf() { + return (Properties) DEFAULT_CONFIG.clone(); + } + + private Properties conf; + private DirectoryService ds; + private KdcServer kdc; + private int port; + private String realm; + private File workDir; + private File krb5conf; + + /** + * Creates a MiniKdc. + * + * @param conf MiniKdc configuration. + * @param workDir working directory, it should be the build directory. Under + * this directory an ApacheDS working directory will be created, + * this directory will be deleted when the MiniKdc stops. + * @throws Exception thrown if the MiniKdc could not be created. + */ + public MiniKdc(Properties conf, File workDir) throws Exception { + if (!conf.keySet().containsAll(PROPERTIES)) { + Set missingProperties = new HashSet(PROPERTIES); + missingProperties.removeAll(conf.keySet()); + throw new IllegalArgumentException("Missing configuration properties: " + missingProperties); + } + this.workDir = new File(workDir, Long.toString(System.currentTimeMillis())); + if (!workDir.exists() && !workDir.mkdirs()) { + throw new RuntimeException("Cannot create directory " + workDir); + } + LOG.info("Configuration:"); + LOG.info("---------------------------------------------------------------"); + for (Map.Entry entry : conf.entrySet()) { + LOG.info(" {}: {}", entry.getKey(), entry.getValue()); + } + LOG.info("---------------------------------------------------------------"); + this.conf = conf; + port = Integer.parseInt(conf.getProperty(KDC_PORT)); + if (port == 0) { + ServerSocket ss = new ServerSocket(0, 1, InetAddress.getByName(conf.getProperty(KDC_BIND_ADDRESS))); + port = ss.getLocalPort(); + ss.close(); + } + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + realm = orgName.toUpperCase() + "." + orgDomain.toUpperCase(); + } + + /** + * Returns the port of the MiniKdc. + * + * @return the port of the MiniKdc. + */ + public int getPort() { + return port; + } + + /** + * Returns the host of the MiniKdc. + * + * @return the host of the MiniKdc. + */ + public String getHost() { + return conf.getProperty(KDC_BIND_ADDRESS); + } + + /** + * Returns the realm of the MiniKdc. + * + * @return the realm of the MiniKdc. + */ + public String getRealm() { + return realm; + } + + public File getKrb5conf() { + return krb5conf; + } + + /** + * Starts the MiniKdc. + * + * @throws Exception thrown if the MiniKdc could not be started. + */ + public synchronized void start() throws Exception { + if (kdc != null) { + throw new RuntimeException("Already started"); + } + initDirectoryService(); + initKDCServer(); + } + + private void initDirectoryService() throws Exception { + ds = new DefaultDirectoryService(); + ds.setInstanceLayout(new InstanceLayout(workDir)); + + CacheService cacheService = new CacheService(); + ds.setCacheService(cacheService); + + // first load the schema + InstanceLayout instanceLayout = ds.getInstanceLayout(); + File schemaPartitionDirectory = new File(instanceLayout.getPartitionsDirectory(), "schema"); + SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor(instanceLayout.getPartitionsDirectory()); + extractor.extractOrCopy(); + + SchemaLoader loader = new LdifSchemaLoader(schemaPartitionDirectory); + SchemaManager schemaManager = new DefaultSchemaManager(loader); + schemaManager.loadAllEnabled(); + ds.setSchemaManager(schemaManager); + // Init the LdifPartition with schema + LdifPartition schemaLdifPartition = new LdifPartition(schemaManager); + schemaLdifPartition.setPartitionPath(schemaPartitionDirectory.toURI()); + + // The schema partition + SchemaPartition schemaPartition = new SchemaPartition(schemaManager); + schemaPartition.setWrappedPartition(schemaLdifPartition); + ds.setSchemaPartition(schemaPartition); + + JdbmPartition systemPartition = new JdbmPartition(ds.getSchemaManager()); + systemPartition.setId("system"); + systemPartition.setPartitionPath(new File(ds.getInstanceLayout().getPartitionsDirectory(), systemPartition + .getId()).toURI()); + systemPartition.setSuffixDn(new Dn(ServerDNConstants.SYSTEM_DN)); + systemPartition.setSchemaManager(ds.getSchemaManager()); + ds.setSystemPartition(systemPartition); + + ds.getChangeLog().setEnabled(false); + ds.setDenormalizeOpAttrsEnabled(true); + ds.addLast(new KeyDerivationInterceptor()); + + // create one partition + String orgName = conf.getProperty(ORG_NAME).toLowerCase(); + String orgDomain = conf.getProperty(ORG_DOMAIN).toLowerCase(); + + JdbmPartition partition = new JdbmPartition(ds.getSchemaManager()); + partition.setId(orgName); + partition.setPartitionPath(new File(ds.getInstanceLayout().getPartitionsDirectory(), orgName).toURI()); + partition.setSuffixDn(new Dn("dc=" + orgName + ",dc=" + orgDomain)); + ds.addPartition(partition); + // indexes + Set> indexedAttributes = new HashSet>(); + indexedAttributes.add(new JdbmIndex("objectClass", false)); + indexedAttributes.add(new JdbmIndex("dc", false)); + indexedAttributes.add(new JdbmIndex("ou", false)); + partition.setIndexedAttributes(indexedAttributes); + + // And start the ds + ds.setInstanceId(conf.getProperty(INSTANCE)); + ds.startup(); + // context entry, after ds.startup() + Dn dn = new Dn("dc=" + orgName + ",dc=" + orgDomain); + Entry entry = ds.newEntry(dn); + entry.add("objectClass", "top", "domain"); + entry.add("dc", orgName); + ds.getAdminSession().add(entry); + } + + private void initKDCServer() throws Exception { + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + String bindAddress = conf.getProperty(KDC_BIND_ADDRESS); + final Map map = new HashMap(); + map.put("0", orgName.toLowerCase()); + map.put("1", orgDomain.toLowerCase()); + map.put("2", orgName.toUpperCase()); + map.put("3", orgDomain.toUpperCase()); + map.put("4", bindAddress); + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + InputStream is1 = cl.getResourceAsStream("minikdc.ldiff"); + + SchemaManager schemaManager = ds.getSchemaManager(); + LdifReader reader = null; + + try { + final String content = StrSubstitutor.replace(IOUtils.toString(is1), map); + reader = new LdifReader(new StringReader(content)); + + for (LdifEntry ldifEntry : reader) { + ds.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry())); + } + } finally { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(is1); + } + + kdc = new KdcServer(); + kdc.setDirectoryService(ds); + + // transport + String transport = conf.getProperty(TRANSPORT); + if (transport.trim().equals("TCP")) { + kdc.addTransports(new TcpTransport(bindAddress, port, 3, 50)); + } else if (transport.trim().equals("UDP")) { + kdc.addTransports(new UdpTransport(port)); + } else { + throw new IllegalArgumentException("Invalid transport: " + transport); + } + kdc.setServiceName(conf.getProperty(INSTANCE)); + kdc.getConfig().setMaximumRenewableLifetime(Long.parseLong(conf.getProperty(MAX_RENEWABLE_LIFETIME))); + kdc.getConfig().setMaximumTicketLifetime(Long.parseLong(conf.getProperty(MAX_TICKET_LIFETIME))); + + kdc.getConfig().setPaEncTimestampRequired(false); + kdc.start(); + + StringBuilder sb = new StringBuilder(); + InputStream is2 = cl.getResourceAsStream("minikdc-krb5.conf"); + + BufferedReader r = null; + + try { + r = new BufferedReader(new InputStreamReader(is2, Charsets.UTF_8)); + String line = r.readLine(); + + while (line != null) { + sb.append(line).append("{3}"); + line = r.readLine(); + } + } finally { + IOUtils.closeQuietly(r); + IOUtils.closeQuietly(is2); + } + + krb5conf = new File(workDir, "krb5.conf").getAbsoluteFile(); + FileUtils.writeStringToFile( + krb5conf, + MessageFormat.format(sb.toString(), getRealm(), getHost(), Integer.toString(getPort()), + System.getProperty("line.separator"))); + System.setProperty("java.security.krb5.conf", krb5conf.getAbsolutePath()); + + System.setProperty("sun.security.krb5.debug", conf.getProperty(DEBUG, "false")); + + // refresh the config + Class classRef; + if (System.getProperty("java.vendor").contains("IBM")) { + classRef = Class.forName("com.ibm.security.krb5.internal.Config"); + } else { + classRef = Class.forName("sun.security.krb5.Config"); + } + Method refreshMethod = classRef.getMethod("refresh", new Class[0]); + refreshMethod.invoke(classRef, new Object[0]); + + LOG.info("MiniKdc listening at port: {}", getPort()); + LOG.info("MiniKdc setting JVM krb5.conf to: {}", krb5conf.getAbsolutePath()); + } + + /** + * Stops the MiniKdc + * + * @throws Exception + */ + public synchronized void stop() { + if (kdc != null) { + System.getProperties().remove("java.security.krb5.conf"); + System.getProperties().remove("sun.security.krb5.debug"); + kdc.stop(); + try { + ds.shutdown(); + } catch (Exception ex) { + LOG.error("Could not shutdown ApacheDS properly: {}", ex.toString(), ex); + } + } + delete(workDir); + } + + private void delete(File f) { + if (f.isFile()) { + if (!f.delete()) { + LOG.warn("WARNING: cannot delete file " + f.getAbsolutePath()); + } + } else { + for (File c : f.listFiles()) { + 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 { + String orgName = conf.getProperty(ORG_NAME); + String orgDomain = conf.getProperty(ORG_DOMAIN); + String baseDn = "ou=users,dc=" + orgName.toLowerCase() + ",dc=" + orgDomain.toLowerCase(); + String content = "dn: uid=" + principal + "," + baseDn + "\n" + "objectClass: top\n" + "objectClass: person\n" + + "objectClass: inetOrgPerson\n" + "objectClass: krb5principal\n" + "objectClass: krb5kdcentry\n" + + "cn: " + principal + "\n" + "sn: " + principal + "\n" + "uid: " + principal + "\n" + "userPassword: " + + password + "\n" + "krb5PrincipalName: " + principal + "@" + getRealm() + "\n" + + "krb5KeyVersionNumber: 0"; + + for (LdifEntry ldifEntry : new LdifReader(new StringReader(content))) { + ds.getAdminSession().add(new DefaultEntry(ds.getSchemaManager(), ldifEntry.getEntry())); + } + } + + /** + * Creates multiple principals in the KDC and adds them to a keytab file. + * + * @param keytabFile keytab file to add the created principal.s + * @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 void createPrincipal(File keytabFile, String... principals) throws Exception { + String generatedPassword = UUID.randomUUID().toString(); + Keytab keytab = new Keytab(); + List entries = new ArrayList(); + for (String principal : principals) { + createPrincipal(principal, generatedPassword); + principal = principal + "@" + getRealm(); + KerberosTime timestamp = new KerberosTime(); + for (Map.Entry entry : KerberosKeyFactory.getKerberosKeys(principal, + generatedPassword).entrySet()) { + EncryptionKey ekey = entry.getValue(); + byte keyVersion = (byte) ekey.getKeyVersion(); + entries.add(new KeytabEntry(principal, 1L, timestamp, keyVersion, ekey)); + } + } + keytab.setEntries(entries); + keytab.write(keytabFile); + } +} diff --git a/spring-security-kerberos-test/src/test/java/org/springframework/security/extensions/kerberos/test/TestMiniKdc.java b/spring-security-kerberos-test/src/test/java/org/springframework/security/extensions/kerberos/test/TestMiniKdc.java new file mode 100644 index 0000000..02085cf --- /dev/null +++ b/spring-security-kerberos-test/src/test/java/org/springframework/security/extensions/kerberos/test/TestMiniKdc.java @@ -0,0 +1,156 @@ +/* + * 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.extensions.kerberos.test; + +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.security.extensions.kerberos.test.KerberosSecurityTestcase; +import org.springframework.security.extensions.kerberos.test.MiniKdc; + +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 java.io.File; +import java.security.Principal; +import java.util.Set; +import java.util.Map; +import java.util.HashSet; +import java.util.HashMap; +import java.util.Arrays; + +public class TestMiniKdc extends KerberosSecurityTestcase { + + @Test + public void testMiniKdcStart() { + MiniKdc kdc = getKdc(); + Assert.assertNotSame(0, kdc.getPort()); + } + + @Test + public void testKeytabGen() throws Exception { + MiniKdc kdc = getKdc(); + File workDir = getWorkDir(); + + kdc.createPrincipal(new File(workDir, "keytab"), "foo/bar", "bar/foo"); + Keytab kt = Keytab.read(new File(workDir, "keytab")); + Set principals = new HashSet(); + for (KeytabEntry entry : kt.getEntries()) { + principals.add(entry.getPrincipalName()); + } + // here principals use \ instead of / + // because + // org.apache.directory.server.kerberos.shared.keytab.KeytabDecoder + // .getPrincipalName(IoBuffer buffer) use \\ when generates principal + Assert.assertEquals( + new HashSet(Arrays.asList("foo\\bar@" + kdc.getRealm(), "bar\\foo@" + kdc.getRealm())), + principals); + } + + private static 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; + } + + public static Configuration createClientConfig(String principal, File keytab) { + return new KerberosConfiguration(principal, keytab, true); + } + + public static Configuration createServerConfig(String principal, File keytab) { + return new KerberosConfiguration(principal, keytab, false); + } + + private static String getKrb5LoginModuleName() { + return System.getProperty("java.vendor").contains("IBM") ? "com.ibm.security.auth.module.Krb5LoginModule" + : "com.sun.security.auth.module.Krb5LoginModule"; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap(); + options.put("keyTab", keytab); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + options.put("useTicketCache", "true"); + options.put("renewTGT", "true"); + options.put("refreshKrb5Config", "true"); + options.put("isInitiator", Boolean.toString(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) }; + } + } + + @Test + public void testKerberosLogin() throws Exception { + MiniKdc kdc = getKdc(); + File workDir = getWorkDir(); + LoginContext loginContext = null; + try { + String principal = "foo"; + File keytab = new File(workDir, "foo.keytab"); + kdc.createPrincipal(keytab, principal); + + Set principals = new HashSet(); + principals.add(new KerberosPrincipal(principal)); + + // client login + Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); + loginContext = new LoginContext("", subject, null, KerberosConfiguration.createClientConfig(principal, + keytab)); + loginContext.login(); + subject = loginContext.getSubject(); + Assert.assertEquals(1, subject.getPrincipals().size()); + Assert.assertEquals(KerberosPrincipal.class, subject.getPrincipals().iterator().next().getClass()); + Assert.assertEquals(principal + "@" + kdc.getRealm(), subject.getPrincipals().iterator().next().getName()); + loginContext.logout(); + + // server login + subject = new Subject(false, principals, new HashSet(), new HashSet()); + loginContext = new LoginContext("", subject, null, KerberosConfiguration.createServerConfig(principal, + keytab)); + loginContext.login(); + subject = loginContext.getSubject(); + Assert.assertEquals(1, subject.getPrincipals().size()); + Assert.assertEquals(KerberosPrincipal.class, subject.getPrincipals().iterator().next().getClass()); + Assert.assertEquals(principal + "@" + kdc.getRealm(), subject.getPrincipals().iterator().next().getName()); + loginContext.logout(); + + } finally { + if (loginContext != null) { + loginContext.logout(); + } + } + } + +} diff --git a/spring-security-kerberos-test/src/test/resources/log4j.properties b/spring-security-kerberos-test/src/test/resources/log4j.properties new file mode 100644 index 0000000..42ac2a2 --- /dev/null +++ b/spring-security-kerberos-test/src/test/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n + +log4j.category.org.springframework.boot=INFO +xlog4j.category.org.apache.http.wire=TRACE +xlog4j.category.org.apache.http.headers=TRACE + diff --git a/spring-security-kerberos-test/src/test/resources/minikdc-krb5.conf b/spring-security-kerberos-test/src/test/resources/minikdc-krb5.conf new file mode 100644 index 0000000..d118dd1 --- /dev/null +++ b/spring-security-kerberos-test/src/test/resources/minikdc-krb5.conf @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# 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. +# +[libdefaults] + default_realm = {0} + udp_preference_limit = 1 + +[realms] + {0} = '{' + kdc = {1}:{2} + '}' \ No newline at end of file diff --git a/spring-security-kerberos-test/src/test/resources/minikdc.ldiff b/spring-security-kerberos-test/src/test/resources/minikdc.ldiff new file mode 100644 index 0000000..603ccb5 --- /dev/null +++ b/spring-security-kerberos-test/src/test/resources/minikdc.ldiff @@ -0,0 +1,47 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# 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. +# +dn: ou=users,dc=${0},dc=${1} +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: uid=krbtgt,ou=users,dc=${0},dc=${1} +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: KDC Service +sn: Service +uid: krbtgt +userPassword: secret +krb5PrincipalName: krbtgt/${2}.${3}@${2}.${3} +krb5KeyVersionNumber: 0 + +dn: uid=ldap,ou=users,dc=${0},dc=${1} +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: LDAP +sn: Service +uid: ldap +userPassword: secret +krb5PrincipalName: ldap/${4}@${2}.${3} +krb5KeyVersionNumber: 0 \ No newline at end of file