From 84cb1ab36e2e58b0b006bef3c9006bbd52578128 Mon Sep 17 00:00:00 2001 From: Mike Wiesner Date: Wed, 23 Dec 2009 14:13:21 +0000 Subject: [PATCH] SES-10: Enable server side Kerberos login --- .../KerberosAuthenticationProvider.java | 72 +++++++++ .../extensions/kerberos/KerberosClient.java | 29 ++++ .../kerberos/SunJaasKerberosClient.java | 146 ++++++++++++++++++ .../KerberosAuthenticationProviderTest.java | 79 ++++++++++ 4 files changed, 326 insertions(+) create mode 100644 spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProvider.java create mode 100644 spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosClient.java create mode 100644 spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosClient.java create mode 100644 spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProviderTest.java diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProvider.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProvider.java new file mode 100644 index 0000000..feb6634 --- /dev/null +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright 2009 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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.authentication.AuthenticationProvider; +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.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +/** + * @author Mike Wiesner + * @since 1.0 + * @version $Id$ + */ +public class KerberosAuthenticationProvider implements AuthenticationProvider { + + private static final Log LOG = LogFactory.getLog(KerberosAuthenticationProvider.class); + + private KerberosClient kerberosClient; + private UserDetailsService userDetailsService; + + + + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication; + String validatedUsername = kerberosClient.login(auth.getName(), auth.getCredentials().toString()); + if (validatedUsername.equalsIgnoreCase(auth.getName()) == false) { + if (LOG.isDebugEnabled()) { + LOG.info("Username returned from KDC ("+validatedUsername+") doesn't match with supplied username ("+auth.getName()+")"); + } + throw new BadCredentialsException("Username returned from KDC doesn't match with supplied username"); + } + + UserDetails userDetails = this.userDetailsService.loadUserByUsername(auth.getName()); + UsernamePasswordAuthenticationToken output = new UsernamePasswordAuthenticationToken(userDetails, auth.getCredentials(), userDetails.getAuthorities()); + return output; + + } + + public boolean supports(Class authentication) { + return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); + } + + public void setKerberosClient(KerberosClient kerberosClient) { + this.kerberosClient = kerberosClient; + } + + + public void setUserDetailsService(UserDetailsService detailsService) { + this.userDetailsService = detailsService; + } + +} diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosClient.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosClient.java new file mode 100644 index 0000000..2ff0682 --- /dev/null +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosClient.java @@ -0,0 +1,29 @@ +/* + * Copyright 2009 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; + +/** + * + * @author Mike Wiesner + * @since 1.0 + * @version $Id$ + */ +public interface KerberosClient { + + public String login(String username, String password); + +} diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosClient.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosClient.java new file mode 100644 index 0000000..49a27a2 --- /dev/null +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosClient.java @@ -0,0 +1,146 @@ +/* + * Copyright 2009 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; + +import java.io.IOException; +import java.util.HashMap; + +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.beans.factory.InitializingBean; +import org.springframework.security.authentication.BadCredentialsException; + +/** + * 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 + * @since 1.0 + * @version $Id$ + */ +public class SunJaasKerberosClient implements KerberosClient, InitializingBean { + + private boolean debug = false; + private String krbConfLocation; + + + private static final Log LOG = LogFactory.getLog(SunJaasKerberosClient.class); + + @Override + public String login(String username, String password) { + LOG.debug("Trying to authenticate " + username + " with Kerberos"); + String validatedUsername; + + try { + LoginContext loginContext = new LoginContext("", null, new KerberosClientCallbackHandler(username, password), + new LoginConfig(this.debug)); + loginContext.login(); + if (LOG.isDebugEnabled()) { + LOG.debug("Kerberos authenticated user: "+loginContext.getSubject()); + } + validatedUsername = loginContext.getSubject().getPrincipals().iterator().next().toString(); + loginContext.logout(); + } catch (LoginException e) { + throw new BadCredentialsException("Kerberos authentication failed", e); + } + return validatedUsername; + + } + + public void setDebug(boolean debug) { + this.debug = debug; + } + + + public void setKrbConfLocation(String krbConfLocation) { + this.krbConfLocation = krbConfLocation; + } + + public void afterPropertiesSet() throws Exception { + if (krbConfLocation != null) { + System.setProperty("java.security.krb5.conf", krbConfLocation); + } + if (debug) { + System.setProperty("sun.security.krb5.debug", "true"); + } + + + } + + private static class LoginConfig extends Configuration { + private boolean debug; + + public LoginConfig(boolean debug) { + super(); + this.debug = debug; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + HashMap options = new HashMap(); + options.put("storeKey", "true"); + if (debug) { + options.put("debug", "true"); + } + + return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), }; + } + + } + + private static class KerberosClientCallbackHandler implements CallbackHandler { + private String username; + private String password; + + public 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(username); + } else if (callback instanceof PasswordCallback) { + PasswordCallback pwcb = (PasswordCallback) callback; + pwcb.setPassword(password.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, "We got a " + callback.getClass().getCanonicalName() + + ", but only NameCallback and PasswordCallback is supported"); + } + } + + } + + } + +} diff --git a/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProviderTest.java b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProviderTest.java new file mode 100644 index 0000000..4ee6783 --- /dev/null +++ b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosAuthenticationProviderTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009 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; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.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; + +/** + * Test class for {@link KerberosAuthenticationProvider} + * + * @author Mike Wiesner + * @since 1.0 + * @version $Id$ + */ +public class KerberosAuthenticationProviderTest { + + private KerberosAuthenticationProvider provider; + private KerberosClient kerberosClient; + private UserDetailsService userDetailsService; + + private static final String TEST_USER = "Testuser@SPRINGSOURCE.ORG"; + private static final String TEST_PASSWORD = "password"; + private static final UsernamePasswordAuthenticationToken INPUT_TOKEN = new UsernamePasswordAuthenticationToken(TEST_USER, TEST_PASSWORD); + private static final List AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN"); + private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true,true, AUTHORITY_LIST); + + @Before + public void before() { + // mocking + this.kerberosClient = mock(KerberosClient.class); + this.userDetailsService = mock(UserDetailsService.class); + this.provider = new KerberosAuthenticationProvider(); + this.provider.setKerberosClient(kerberosClient); + this.provider.setUserDetailsService(userDetailsService); + } + + @Test + public void testLoginOk() throws Exception { + when(userDetailsService.loadUserByUsername(TEST_USER)).thenReturn(USER_DETAILS); + when(kerberosClient.login(TEST_USER, TEST_PASSWORD)).thenReturn(TEST_USER); + + Authentication authenticate = provider.authenticate(INPUT_TOKEN); + + verify(kerberosClient).login(TEST_USER, TEST_PASSWORD); + + assertNotNull(authenticate); + assertEquals(TEST_USER, authenticate.getName()); + assertEquals(USER_DETAILS, authenticate.getPrincipal()); + assertEquals(TEST_PASSWORD, authenticate.getCredentials()); + assertEquals(AUTHORITY_LIST, authenticate.getAuthorities()); + + } +}