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 index 12b5fbd..ae940aa 100644 --- 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 @@ -137,6 +137,37 @@ public class KerberosRestTemplateTests extends KerberosSecurityTestcase { assertThat(response, is("login")); } + @Test + public void testSpnegoWithSuccessHandler() throws Exception { + + MiniKdc kdc = getKdc(); + File workDir = getWorkDir(); + String host = InetAddress.getLocalHost().getCanonicalHostName(); + + String serverPrincipal = "HTTP/" + host; + File serverKeytab = new File(workDir, "server.keytab"); + kdc.createPrincipal(serverKeytab, serverPrincipal); + + String clientPrincipal = "client/" + host; + File clientKeytab = new File(workDir, "client.keytab"); + kdc.createPrincipal(clientKeytab, clientPrincipal); + + + context = SpringApplication.run(new Object[] { WebSecurityConfigSuccessHandler.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://" + host + ":" + port + "/hello", String.class); + assertThat(response, is("home")); + } + protected static class PortInitListener implements ApplicationListener { public int port; diff --git a/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfigSuccessHandler.java b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfigSuccessHandler.java new file mode 100644 index 0000000..4171d32 --- /dev/null +++ b/spring-security-kerberos-client/src/test/java/org/springframework/security/extensions/kerberos/client/WebSecurityConfigSuccessHandler.java @@ -0,0 +1,115 @@ +/* + * 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.ResponseHeaderSettingKerberosAuthenticationSuccessHandler; +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 WebSecurityConfigSuccessHandler 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(); + + ResponseHeaderSettingKerberosAuthenticationSuccessHandler successHandler = new ResponseHeaderSettingKerberosAuthenticationSuccessHandler(); + filter.setSuccessHandler(successHandler); + + 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-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProvider.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProvider.java index ada78e6..9474858 100644 --- a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProvider.java +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProvider.java @@ -25,6 +25,7 @@ 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.security.extensions.kerberos.KerberosTicketValidator.KerberosTicketValidation; import org.springframework.security.extensions.kerberos.web.SpnegoAuthenticationProcessingFilter; import org.springframework.util.Assert; @@ -42,6 +43,7 @@ import org.springframework.util.Assert; * You can see an example configuration in SpnegoAuthenticationProcessingFilter. * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 * @see KerberosTicketValidator * @see UserDetailsService @@ -56,36 +58,22 @@ public class KerberosServiceAuthenticationProvider implements private UserDetailsService userDetailsService; private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); - - /** The UserDetailsService to use, for loading the user properties - * and the GrantedAuthorities. - */ - public void setUserDetailsService(UserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; - } - - /** The KerberosTicketValidator to use, for validating - * the Kerberos/SPNEGO tickets. - */ - public void setTicketValidator(KerberosTicketValidator ticketValidator) { - this.ticketValidator = ticketValidator; - } - @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { KerberosServiceRequestToken auth = (KerberosServiceRequestToken) authentication; byte[] token = auth.getToken(); LOG.debug("Try to validate Kerberos Token"); - String username = this.ticketValidator.validateTicket(token); - LOG.debug("Succesfully validated " + username); - UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + KerberosTicketValidation ticketValidation = this.ticketValidator.validateTicket(token); + LOG.debug("Succesfully validated " + ticketValidation.username()); + UserDetails userDetails = this.userDetailsService.loadUserByUsername(ticketValidation.username()); userDetailsChecker.check(userDetails); additionalAuthenticationChecks(userDetails, auth); - KerberosServiceRequestToken responseAuth = new KerberosServiceRequestToken(userDetails, userDetails.getAuthorities(), token); + KerberosServiceRequestToken responseAuth = new KerberosServiceRequestToken( + userDetails, ticketValidation, + userDetails.getAuthorities(), token); responseAuth.setDetails(authentication.getDetails()); return responseAuth; - } @Override @@ -99,6 +87,26 @@ public class KerberosServiceAuthenticationProvider implements Assert.notNull(this.userDetailsService, "userDetailsService must be specified"); } + /** + * The UserDetailsService to use, for loading the user properties + * and the GrantedAuthorities. + * + * @param userDetailsService the new user details service + */ + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + /** + * The KerberosTicketValidator to use, for validating + * the Kerberos/SPNEGO tickets. + * + * @param ticketValidator the new ticket validator + */ + public void setTicketValidator(KerberosTicketValidator ticketValidator) { + this.ticketValidator = ticketValidator; + } + /** * Allows subclasses to perform any additional checks of a returned UserDetails * for a given authentication request. @@ -110,7 +118,6 @@ public class KerberosServiceAuthenticationProvider implements */ protected void additionalAuthenticationChecks(UserDetails userDetails, KerberosServiceRequestToken authentication) throws AuthenticationException { - } } diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceRequestToken.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceRequestToken.java index a5479d2..1cc5f04 100644 --- a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceRequestToken.java +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosServiceRequestToken.java @@ -15,50 +15,70 @@ */ package org.springframework.security.extensions.kerberos; +import java.io.UnsupportedEncodingException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.util.Arrays; 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.userdetails.UserDetails; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.extensions.kerberos.KerberosTicketValidator.KerberosTicketValidation; import org.springframework.security.extensions.kerberos.web.SpnegoAuthenticationProcessingFilter; /** - * Holds the Kerberos/SPNEGO token for requesting a kerberized service - * and is also the output of KerberosServiceAuthenticationProvider.
+ * Holds the Kerberos/SPNEGO token for requesting a kerberized service and is + * also the output of KerberosServiceAuthenticationProvider.
* Will mostly be created in SpnegoAuthenticationProcessingFilter * and authenticated in KerberosServiceAuthenticationProvider. * - * This token cannot be re-authenticated, as you will get a Kerberos Reply error. + * This token cannot be re-authenticated, as you will get a Kerberos Reply + * error. * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 * @see KerberosServiceAuthenticationProvider * @see SpnegoAuthenticationProcessingFilter */ - public class KerberosServiceRequestToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 395488921064775014L; + private final byte[] token; + private final Object principal; - /** Creates an authenticated token, normally used as an output of an authentication provider. - * @param principal the user principal (mostly of instance UserDetails + private final transient KerberosTicketValidation ticketValidation; + + /** + * Creates an authenticated token, normally used as an output of an + * authentication provider. + * + * @param principal the user principal (mostly of instance UserDetails) + * @param ticketValidation result of ticket validation * @param authorities the authorities which are granted to the user * @param token the Kerberos/SPNEGO token * @see UserDetails */ - public KerberosServiceRequestToken(Object principal, Collection authorities, byte[] token) { + public KerberosServiceRequestToken(Object principal, KerberosTicketValidation ticketValidation, + Collection authorities, byte[] token) { super(authorities); this.token = token; this.principal = principal; + this.ticketValidation = ticketValidation; super.setAuthenticated(true); } /** * Creates an unauthenticated instance which should then be authenticated by - * KerberosServiceAuthenticationProvider/code> + * KerberosServiceAuthenticationProvider/code>. * * @param token Kerberos/SPNEGO token * @see KerberosServiceAuthenticationProvider @@ -66,6 +86,7 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken { public KerberosServiceRequestToken(byte[] token) { super(null); this.token = token; + this.ticketValidation = null; this.principal = null; } @@ -114,4 +135,96 @@ public class KerberosServiceRequestToken extends AbstractAuthenticationToken { return this.token; } + /** + * Gets the ticket validation + * + * @return the ticket validation (which will be null if the token is unauthenticated) + */ + public KerberosTicketValidation getTicketValidation() { + return ticketValidation; + } + + /** + * Determines whether an authenticated token has a response token + * + * @return whether a response token is available + */ + public boolean hasResponseToken() { + return ticketValidation != null && 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"); + + try { + return new String(Base64.encode(ticketValidation.responseToken()), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Unable to encode response token", e); + } + } + + /** + * 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 + */ + public byte[] decrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException { + return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction() { + public byte[] run() throws Exception { + final GSSContext context = getTicketValidation().getGssContext(); + return context.unwrap(data, offset, length, new MessageProp(true)); + } + }); + } + + /** + * Unwraps an encrypted message using the gss context + * + * @param data + * @return the decrypted message + * @throws PrivilegedActionException + */ + public byte[] decrypt(final byte[] data) throws PrivilegedActionException { + return decrypt(data, 0, data.length); + } + + /** + * Wraps an message using the gss context + * + * @param data + * @param offset + * @param length + * @return the encrypted message + * @throws PrivilegedActionException + */ + public byte[] encrypt(final byte[] data, final int offset, final int length) throws PrivilegedActionException { + return Subject.doAs(getTicketValidation().subject(), new PrivilegedExceptionAction() { + public byte[] run() throws Exception { + final GSSContext context = getTicketValidation().getGssContext(); + return context.wrap(data, offset, length, new MessageProp(true)); + } + }); + } + + /** + * Wraps an message using the gss context + * + * @param data + * @return the encrypted message + * @throws PrivilegedActionException + */ + public byte[] encrypt(final byte[] data) throws PrivilegedActionException { + return encrypt(data, 0, data.length); + } + } diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosTicketValidator.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosTicketValidator.java index 9d72d7a..cfc4331 100644 --- a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosTicketValidator.java +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/KerberosTicketValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2009 the original author or authors. + * Copyright 2009-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. @@ -13,27 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.security.extensions.kerberos; +import java.util.HashSet; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; + +import org.ietf.jgss.GSSContext; import org.springframework.security.authentication.BadCredentialsException; /** * Implementations of this interface are used in - * {@link KerberosServiceAuthenticationProvider} to validate a Kerberos/SPNEGO Ticket. + * {@link KerberosServiceAuthenticationProvider} to validate a Kerberos/SPNEGO + * Ticket. * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 - * @version $Id$ * @see KerberosServiceAuthenticationProvider */ public interface KerberosTicketValidator { - /** Validates a Kerberos/SPNEGO ticket. + /** + * Validates a Kerberos/SPNEGO ticket. + * * @param token Kerbeos/SPNEGO ticket * @return authenticated kerberos principal * @throws BadCredentialsException if the ticket is not valid */ - public String validateTicket(byte[] token) throws BadCredentialsException; + public KerberosTicketValidation validateTicket(byte[] token) + throws BadCredentialsException; -} \ No newline at end of file + /** + * Result of ticket validation + */ + public static class KerberosTicketValidation { + + private final String username; + private final byte[] responseToken; + private final GSSContext gssContext; + private final String servicePrincipal; + + KerberosTicketValidation(String username, String servicePrincipal, byte[] responseToken, GSSContext gssContext) { + this.username = username; + this.servicePrincipal = servicePrincipal; + this.responseToken = responseToken; + this.gssContext = gssContext; + } + + public String username() { + return username; + } + + public byte[] responseToken() { + return responseToken; + } + + public GSSContext getGssContext() { + return gssContext; + } + + public Subject subject() { + final HashSet princs = new HashSet(); + princs.add(new KerberosPrincipal(servicePrincipal)); + return new Subject(false, princs, new HashSet(), new HashSet()); + } + + } +} diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosTicketValidator.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosTicketValidator.java index a1d9e45..59ff0cf 100644 --- a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosTicketValidator.java +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/SunJaasKerberosTicketValidator.java @@ -46,6 +46,7 @@ import org.springframework.util.Assert; * is needed. * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 */ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, InitializingBean { @@ -53,18 +54,18 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, private String servicePrincipal; private Resource keyTabLocation; private Subject serviceSubject; + private boolean holdOnToGSSContext; private boolean debug = false; private static final Log LOG = LogFactory.getLog(SunJaasKerberosTicketValidator.class); @Override - public String validateTicket(byte[] token) { - String username = null; + public KerberosTicketValidation validateTicket(byte[] token) { try { - username = Subject.doAs(this.serviceSubject, new KerberosValidateAction(token)); - } catch (PrivilegedActionException e) { - throw new BadCredentialsException("Kerberos validation not succesfull", e); + return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token)); + } + catch (PrivilegedActionException e) { + throw new BadCredentialsException("Kerberos validation not succesful", e); } - return username; } @Override @@ -120,32 +121,48 @@ public class SunJaasKerberosTicketValidator implements KerberosTicketValidator, this.keyTabLocation = keyTabLocation; } - /** Enables the debug mode of the JAAS Kerberos login module + /** + * Enables the debug mode of the JAAS Kerberos login module. + * * @param debug default is false */ public void setDebug(boolean debug) { this.debug = debug; } + /** + * Determines whether to hold on to the {@link GSSContext GSS security context} or + * otherwise {@link GSSContext#dispose() dispose} of it immediately (the default behaviour). + *

Holding on to the GSS context allows decrypt and encrypt operations for subsequent + * interactions with the principal. + * + * @param holdOnToGSSContext true if should hold on to context + */ + public void setHoldOnToGSSContext(boolean holdOnToGSSContext) { + this.holdOnToGSSContext = holdOnToGSSContext; + } + /** * 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 static class KerberosValidateAction implements PrivilegedExceptionAction { + private class KerberosValidateAction implements PrivilegedExceptionAction { byte[] kerberosTicket; public KerberosValidateAction(byte[] kerberosTicket) { this.kerberosTicket = kerberosTicket; } - public String run() throws Exception { - GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null); - context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length); - String user = context.getSrcName().toString(); - context.dispose(); - return user; + @Override + public KerberosTicketValidation run() throws Exception { + GSSContext context = GSSManager.getInstance().createContext((GSSCredential) null); + byte[] responseToken = context.acceptSecContext(kerberosTicket, 0, kerberosTicket.length); + String user = context.getSrcName().toString(); + if (!holdOnToGSSContext) { + context.dispose(); + } + return new KerberosTicketValidation(user, servicePrincipal, responseToken, context); } - } /** diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java new file mode 100644 index 0000000..7b02a16 --- /dev/null +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/ResponseHeaderSettingKerberosAuthenticationSuccessHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-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.web; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.extensions.kerberos.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(headerName, headerPrefix + auth.getEncodedResponseToken()); + } + } + +} diff --git a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilter.java b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilter.java index 2e9bc60..d6bccd9 100644 --- a/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilter.java +++ b/spring-security-kerberos-core/src/main/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilter.java @@ -103,6 +103,7 @@ import org.springframework.web.filter.GenericFilterBean; * * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 * @see KerberosServiceAuthenticationProvider * @see SpnegoEntryPoint @@ -133,11 +134,11 @@ public class SpnegoAuthenticationProcessingFilter extends GenericFilterBean { String header = request.getHeader("Authorization"); - if ((header != null) && header.startsWith("Negotiate ")) { + if (header != null && (header.startsWith("Negotiate ") || header.startsWith("Kerberos "))) { if (logger.isDebugEnabled()) { logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header); } - byte[] base64Token = header.substring(10).getBytes("UTF-8"); + byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8"); byte[] kerberosTicket = Base64.decode(base64Token); KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket); authenticationRequest.setDetails(authenticationDetailsSource.buildDetails(request)); diff --git a/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProviderTest.java b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProviderTest.java index 668a41c..9f871d4 100644 --- a/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProviderTest.java +++ b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/KerberosServiceAuthenticationProviderTest.java @@ -34,11 +34,13 @@ 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.KerberosTicketValidator.KerberosTicketValidation; /** * Test class for {@link KerberosServiceAuthenticationProvider} * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 */ public class KerberosServiceAuthenticationProviderTest { @@ -49,7 +51,11 @@ public class KerberosServiceAuthenticationProviderTest { // 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", RESPONSE_TOKEN, null); + private static final List AUTHORITY_LIST = AuthorityUtils.createAuthorityList("ROLE_ADMIN"); private static final UserDetails USER_DETAILS = new User(TEST_USER, "empty", true, true, true,true, AUTHORITY_LIST); private static final KerberosServiceRequestToken INPUT_TOKEN = new KerberosServiceRequestToken(TEST_TOKEN); @@ -109,7 +115,7 @@ public class KerberosServiceAuthenticationProviderTest { @Test(expected=UsernameNotFoundException.class) public void testUsernameNotFound() throws Exception { // stubbing - when(ticketValidator.validateTicket(TEST_TOKEN)).thenReturn(TEST_USER); + when(ticketValidator.validateTicket(TEST_TOKEN)).thenReturn(TICKET_VALIDATION); when(userDetailsService.loadUserByUsername(TEST_USER)).thenThrow(new UsernameNotFoundException("")); // testing @@ -128,7 +134,7 @@ public class KerberosServiceAuthenticationProviderTest { private Authentication callProviderAndReturnUser(UserDetails userDetails, Authentication inputToken) { // stubbing - when(ticketValidator.validateTicket(TEST_TOKEN)).thenReturn(TEST_USER); + when(ticketValidator.validateTicket(TEST_TOKEN)).thenReturn(TICKET_VALIDATION); when(userDetailsService.loadUserByUsername(TEST_USER)).thenReturn(userDetails); // testing diff --git a/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilterTest.java b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilterTest.java index 626c859..f77887d 100644 --- a/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilterTest.java +++ b/spring-security-kerberos-core/src/test/java/org/springframework/security/extensions/kerberos/web/SpnegoAuthenticationProcessingFilterTest.java @@ -15,8 +15,13 @@ */ package org.springframework.security.extensions.kerberos.web; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; @@ -39,6 +44,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.extensions.kerberos.KerberosServiceRequestToken; +import org.springframework.security.extensions.kerberos.KerberosTicketValidator.KerberosTicketValidation; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -47,27 +53,45 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS * Test class for {@link SpnegoAuthenticationProcessingFilter} * * @author Mike Wiesner + * @author Jeremy Stone * @since 1.0 */ public class SpnegoAuthenticationProcessingFilterTest { 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 final Authentication AUTHENTICATION = new KerberosServiceRequestToken("test", - AuthorityUtils.createAuthorityList("ROLE_ADMIN"), TEST_TOKEN); + + 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 = "Negotiate "; - private static final BadCredentialsException BCE = new BadCredentialsException(""); + + private static final String TOKEN_PREFIX_NEG = "Negotiate "; + + private static final String TOKEN_PREFIX_KERB = "Kerberos "; + + private static final BadCredentialsException BCE = new BadCredentialsException(""); @Before public void before() throws Exception { @@ -84,29 +108,44 @@ public class SpnegoAuthenticationProcessingFilterTest { @Test public void testEverythingWorks() throws Exception { - everythingWorks(); + everythingWorks(TOKEN_PREFIX_NEG); + } + + @Test + public void testEverythingWorks_Kerberos() throws Exception { + everythingWorks(TOKEN_PREFIX_KERB); } @Test public void testEverythingWorksWithHandlers() throws Exception { - createHandler(); - everythingWorks(); - verify(successHandler).onAuthenticationSuccess(request, response, AUTHENTICATION); - verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(AuthenticationException.class)); + everythingWorksWithHandlers(TOKEN_PREFIX_NEG); } - private void everythingWorks() throws IOException, ServletException { + @Test + public void testEverythingWorksWithHandlers_Kerberos() throws Exception { + everythingWorksWithHandlers(TOKEN_PREFIX_KERB); + } + + private void everythingWorksWithHandlers(String tokenPrefix) throws Exception { + createHandler(); + everythingWorks(tokenPrefix); + verify(successHandler).onAuthenticationSuccess(request, response, AUTHENTICATION); + verify(failureHandler, never()).onAuthenticationFailure(any(HttpServletRequest.class), + any(HttpServletResponse.class), any(AuthenticationException.class)); + } + + private void everythingWorks(String tokenPrefix) throws IOException, + ServletException { // stubbing - when(request.getHeader(HEADER)).thenReturn(TOKEN_PREFIX + TEST_TOKEN_BASE64); - KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN); - requestToken.setDetails(detailsSource.buildDetails(request)); - when(authenticationManager.authenticate(requestToken)).thenReturn(AUTHENTICATION); + when(request.getHeader(HEADER)).thenReturn(tokenPrefix + TEST_TOKEN_BASE64); + KerberosServiceRequestToken requestToken = new KerberosServiceRequestToken(TEST_TOKEN); + requestToken.setDetails(detailsSource.buildDetails(request)); + when(authenticationManager.authenticate(requestToken)).thenReturn(AUTHENTICATION); // testing - filter.doFilter(request, response, chain); - verify(chain).doFilter(request, response); - assertEquals(AUTHENTICATION, SecurityContextHolder.getContext().getAuthentication()); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + assertEquals(AUTHENTICATION, SecurityContextHolder.getContext().getAuthentication()); } @Test @@ -114,50 +153,53 @@ public class SpnegoAuthenticationProcessingFilterTest { filter.doFilter(request, response, chain); // If the header is not present, the filter is not allowed to call // authenticate() - verify(authenticationManager, never()).authenticate(any(Authentication.class)); + verify(authenticationManager, never()).authenticate(any(Authentication.class)); // chain should go on - verify(chain).doFilter(request, response); - assertEquals(null, SecurityContextHolder.getContext().getAuthentication()); + verify(chain).doFilter(request, response); + assertEquals(null, SecurityContextHolder.getContext().getAuthentication()); } @Test public void testAuthenticationFails() throws Exception { authenticationFails(); - verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } @Test public void testAuthenticationFailsWithHandlers() throws Exception { - createHandler(); - authenticationFails(); - verify(failureHandler).onAuthenticationFailure(request, response, BCE); - verify(successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Authentication.class)); - verify(response, never()).setStatus(anyInt()); + createHandler(); + authenticationFails(); + verify(failureHandler).onAuthenticationFailure(request, response, BCE); + verify(successHandler, never()).onAuthenticationSuccess(any(HttpServletRequest.class), + any(HttpServletResponse.class), any(Authentication.class)); + verify(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); - when(request.getHeader(HEADER)).thenReturn(TOKEN_PREFIX + TEST_TOKEN_BASE64); - filter.doFilter(request, response, chain); - verify(authenticationManager, never()).authenticate(any(Authentication.class)); - } finally { + Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike", + AuthorityUtils.createAuthorityList("ROLE_TEST")); + SecurityContextHolder.getContext().setAuthentication(existingAuth); + when(request.getHeader(HEADER)).thenReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64); + filter.doFilter(request, response, chain); + verify(authenticationManager, never()).authenticate(any(Authentication.class)); + } + finally { SecurityContextHolder.clearContext(); } } @Test - public void testAlreadyAuthenticatedWithNotAuthenticatedToken() throws Exception { + public void testAlreadyAuthenticatedWithNotAuthenticatedToken() + throws Exception { try { // this token is not authenticated yet! - Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike"); - SecurityContextHolder.getContext().setAuthentication(existingAuth); - everythingWorks(); - } finally { + Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike"); + SecurityContextHolder.getContext().setAuthentication(existingAuth); + everythingWorks(TOKEN_PREFIX_NEG); + } + finally { SecurityContextHolder.clearContext(); } } @@ -165,11 +207,12 @@ public class SpnegoAuthenticationProcessingFilterTest { @Test public void testAlreadyAuthenticatedWithAnonymousToken() throws Exception { try { - Authentication existingAuth = new AnonymousAuthenticationToken("test", "mike", - AuthorityUtils.createAuthorityList("ROLE_TEST")); - SecurityContextHolder.getContext().setAuthentication(existingAuth); - everythingWorks(); - } finally { + Authentication existingAuth = new AnonymousAuthenticationToken("test", "mike", + AuthorityUtils.createAuthorityList("ROLE_TEST")); + SecurityContextHolder.getContext().setAuthentication(existingAuth); + everythingWorks(TOKEN_PREFIX_NEG); + } + finally { SecurityContextHolder.clearContext(); } } @@ -177,26 +220,27 @@ public class SpnegoAuthenticationProcessingFilterTest { @Test public void testAlreadyAuthenticatedNotActive() throws Exception { try { - Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike", - AuthorityUtils.createAuthorityList("ROLE_TEST")); - SecurityContextHolder.getContext().setAuthentication(existingAuth); - filter.setSkipIfAlreadyAuthenticated(false); - everythingWorks(); - } finally { + Authentication existingAuth = new UsernamePasswordAuthenticationToken("mike", "mike", + AuthorityUtils.createAuthorityList("ROLE_TEST")); + SecurityContextHolder.getContext().setAuthentication(existingAuth); + filter.setSkipIfAlreadyAuthenticated(false); + everythingWorks(TOKEN_PREFIX_NEG); + } + finally { SecurityContextHolder.clearContext(); } } private void authenticationFails() throws IOException, ServletException { // stubbing - when(request.getHeader(HEADER)).thenReturn(TOKEN_PREFIX + TEST_TOKEN_BASE64); - when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(BCE); + when(request.getHeader(HEADER)).thenReturn(TOKEN_PREFIX_NEG + TEST_TOKEN_BASE64); + when(authenticationManager.authenticate(any(Authentication.class))).thenThrow(BCE); // testing filter.doFilter(request, response, chain); // chain should stop here and it should send back a 500 // future version should call some error handler - verify(chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + verify(chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); } private void createHandler() {