44 changed files with 0 additions and 4199 deletions
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
apply plugin: 'io.spring.convention.spring-module' |
||||
|
||||
dependencies { |
||||
management platform(project(":spring-security-dependencies")) |
||||
api project(':spring-security-core') |
||||
api project(':spring-security-web') |
||||
api 'org.jasig.cas.client:cas-client-core' |
||||
api 'org.springframework:spring-beans' |
||||
api 'org.springframework:spring-context' |
||||
api 'org.springframework:spring-core' |
||||
api 'org.springframework:spring-web' |
||||
|
||||
optional 'com.fasterxml.jackson.core:jackson-databind' |
||||
|
||||
provided 'jakarta.servlet:jakarta.servlet-api' |
||||
|
||||
testImplementation "org.assertj:assertj-core" |
||||
testImplementation "org.junit.jupiter:junit-jupiter-api" |
||||
testImplementation "org.junit.jupiter:junit-jupiter-params" |
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine" |
||||
testImplementation "org.mockito:mockito-core" |
||||
testImplementation "org.mockito:mockito-junit-jupiter" |
||||
testImplementation "org.springframework:spring-test" |
||||
testImplementation 'org.skyscreamer:jsonassert' |
||||
} |
||||
@ -1,37 +0,0 @@
@@ -1,37 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas; |
||||
|
||||
/** |
||||
* Sets the appropriate parameters for CAS's implementation of SAML (which is not |
||||
* guaranteed to be actually SAML compliant). |
||||
* |
||||
* @author Scott Battaglia |
||||
* @since 3.0 |
||||
*/ |
||||
public final class SamlServiceProperties extends ServiceProperties { |
||||
|
||||
public static final String DEFAULT_SAML_ARTIFACT_PARAMETER = "SAMLart"; |
||||
|
||||
public static final String DEFAULT_SAML_SERVICE_PARAMETER = "TARGET"; |
||||
|
||||
public SamlServiceProperties() { |
||||
super.setArtifactParameter(DEFAULT_SAML_ARTIFACT_PARAMETER); |
||||
super.setServiceParameter(DEFAULT_SAML_SERVICE_PARAMETER); |
||||
} |
||||
|
||||
} |
||||
@ -1,132 +0,0 @@
@@ -1,132 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas; |
||||
|
||||
import org.springframework.beans.factory.InitializingBean; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Stores properties related to this CAS service. |
||||
* <p> |
||||
* Each web application capable of processing CAS tickets is known as a service. This |
||||
* class stores the properties that are relevant to the local CAS service, being the |
||||
* application that is being secured by Spring Security. |
||||
* |
||||
* @author Ben Alex |
||||
*/ |
||||
public class ServiceProperties implements InitializingBean { |
||||
|
||||
public static final String DEFAULT_CAS_ARTIFACT_PARAMETER = "ticket"; |
||||
|
||||
public static final String DEFAULT_CAS_SERVICE_PARAMETER = "service"; |
||||
|
||||
private String service; |
||||
|
||||
private boolean authenticateAllArtifacts; |
||||
|
||||
private boolean sendRenew = false; |
||||
|
||||
private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER; |
||||
|
||||
private String serviceParameter = DEFAULT_CAS_SERVICE_PARAMETER; |
||||
|
||||
@Override |
||||
public void afterPropertiesSet() { |
||||
Assert.hasLength(this.service, "service cannot be empty."); |
||||
Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty."); |
||||
Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty."); |
||||
} |
||||
|
||||
/** |
||||
* Represents the service the user is authenticating to. |
||||
* <p> |
||||
* This service is the callback URL belonging to the local Spring Security System for |
||||
* Spring secured application. For example, |
||||
* |
||||
* <pre> |
||||
* https://www.mycompany.com/application/login/cas
|
||||
* </pre> |
||||
* @return the URL of the service the user is authenticating to |
||||
*/ |
||||
public final String getService() { |
||||
return this.service; |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether the <code>renew</code> parameter should be sent to the CAS login |
||||
* URL and CAS validation URL. |
||||
* <p> |
||||
* If <code>true</code>, it will force CAS to authenticate the user again (even if the |
||||
* user has previously authenticated). During ticket validation it will require the |
||||
* ticket was generated as a consequence of an explicit login. High security |
||||
* applications would probably set this to <code>true</code>. Defaults to |
||||
* <code>false</code>, providing automated single sign on. |
||||
* @return whether to send the <code>renew</code> parameter to CAS |
||||
*/ |
||||
public final boolean isSendRenew() { |
||||
return this.sendRenew; |
||||
} |
||||
|
||||
public final void setSendRenew(final boolean sendRenew) { |
||||
this.sendRenew = sendRenew; |
||||
} |
||||
|
||||
public final void setService(final String service) { |
||||
this.service = service; |
||||
} |
||||
|
||||
public final String getArtifactParameter() { |
||||
return this.artifactParameter; |
||||
} |
||||
|
||||
/** |
||||
* Configures the Request Parameter to look for when attempting to see if a CAS ticket |
||||
* was sent from the server. |
||||
* @param artifactParameter the id to use. Default is "ticket". |
||||
*/ |
||||
public final void setArtifactParameter(final String artifactParameter) { |
||||
this.artifactParameter = artifactParameter; |
||||
} |
||||
|
||||
/** |
||||
* Configures the Request parameter to look for when attempting to send a request to |
||||
* CAS. |
||||
* @return the service parameter to use. Default is "service". |
||||
*/ |
||||
public final String getServiceParameter() { |
||||
return this.serviceParameter; |
||||
} |
||||
|
||||
public final void setServiceParameter(final String serviceParameter) { |
||||
this.serviceParameter = serviceParameter; |
||||
} |
||||
|
||||
public final boolean isAuthenticateAllArtifacts() { |
||||
return this.authenticateAllArtifacts; |
||||
} |
||||
|
||||
/** |
||||
* If true, then any non-null artifact (ticket) should be authenticated. Additionally, |
||||
* the service will be determined dynamically in order to ensure the service matches |
||||
* the expected value for this artifact. |
||||
* @param authenticateAllArtifacts |
||||
*/ |
||||
public final void setAuthenticateAllArtifacts(final boolean authenticateAllArtifacts) { |
||||
this.authenticateAllArtifacts = authenticateAllArtifacts; |
||||
} |
||||
|
||||
} |
||||
@ -1,60 +0,0 @@
@@ -1,60 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import java.util.ArrayList; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.SpringSecurityCoreVersion; |
||||
|
||||
/** |
||||
* Temporary authentication object needed to load the user details service. |
||||
* |
||||
* @author Scott Battaglia |
||||
* @since 3.0 |
||||
*/ |
||||
public final class CasAssertionAuthenticationToken extends AbstractAuthenticationToken { |
||||
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; |
||||
|
||||
private final Assertion assertion; |
||||
|
||||
private final String ticket; |
||||
|
||||
public CasAssertionAuthenticationToken(final Assertion assertion, final String ticket) { |
||||
super(new ArrayList<>()); |
||||
this.assertion = assertion; |
||||
this.ticket = ticket; |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.assertion.getPrincipal().getName(); |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return this.ticket; |
||||
} |
||||
|
||||
public Assertion getAssertion() { |
||||
return this.assertion; |
||||
} |
||||
|
||||
} |
||||
@ -1,244 +0,0 @@
@@ -1,244 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.jasig.cas.client.validation.TicketValidationException; |
||||
import org.jasig.cas.client.validation.TicketValidator; |
||||
|
||||
import org.springframework.beans.factory.InitializingBean; |
||||
import org.springframework.context.MessageSource; |
||||
import org.springframework.context.MessageSourceAware; |
||||
import org.springframework.context.support.MessageSourceAccessor; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker; |
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.cas.web.CasAuthenticationFilter; |
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.SpringSecurityMessageSource; |
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; |
||||
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; |
||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; |
||||
import org.springframework.security.core.userdetails.UserDetailsChecker; |
||||
import org.springframework.security.core.userdetails.UserDetailsService; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link AuthenticationProvider} implementation that integrates with JA-SIG Central |
||||
* Authentication Service (CAS). |
||||
* <p> |
||||
* This <code>AuthenticationProvider</code> is capable of validating |
||||
* {@link UsernamePasswordAuthenticationToken} requests which contain a |
||||
* <code>principal</code> name equal to either |
||||
* {@link CasAuthenticationFilter#CAS_STATEFUL_IDENTIFIER} or |
||||
* {@link CasAuthenticationFilter#CAS_STATELESS_IDENTIFIER}. It can also validate a |
||||
* previously created {@link CasAuthenticationToken}. |
||||
* |
||||
* @author Ben Alex |
||||
* @author Scott Battaglia |
||||
*/ |
||||
public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { |
||||
|
||||
private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class); |
||||
|
||||
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService; |
||||
|
||||
private final UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker(); |
||||
|
||||
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); |
||||
|
||||
private StatelessTicketCache statelessTicketCache = new NullStatelessTicketCache(); |
||||
|
||||
private String key; |
||||
|
||||
private TicketValidator ticketValidator; |
||||
|
||||
private ServiceProperties serviceProperties; |
||||
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); |
||||
|
||||
@Override |
||||
public void afterPropertiesSet() { |
||||
Assert.notNull(this.authenticationUserDetailsService, "An authenticationUserDetailsService must be set"); |
||||
Assert.notNull(this.ticketValidator, "A ticketValidator must be set"); |
||||
Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set"); |
||||
Assert.hasText(this.key, |
||||
"A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated"); |
||||
Assert.notNull(this.messages, "A message source must be set"); |
||||
} |
||||
|
||||
@Override |
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
if (!supports(authentication.getClass())) { |
||||
return null; |
||||
} |
||||
if (authentication instanceof UsernamePasswordAuthenticationToken |
||||
&& (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER.equals(authentication.getPrincipal().toString()) |
||||
&& !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER |
||||
.equals(authentication.getPrincipal().toString()))) { |
||||
// UsernamePasswordAuthenticationToken not CAS related
|
||||
return null; |
||||
} |
||||
// If an existing CasAuthenticationToken, just check we created it
|
||||
if (authentication instanceof CasAuthenticationToken) { |
||||
if (this.key.hashCode() != ((CasAuthenticationToken) authentication).getKeyHash()) { |
||||
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.incorrectKey", |
||||
"The presented CasAuthenticationToken does not contain the expected key")); |
||||
} |
||||
return authentication; |
||||
} |
||||
|
||||
// Ensure credentials are presented
|
||||
if ((authentication.getCredentials() == null) || "".equals(authentication.getCredentials())) { |
||||
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.noServiceTicket", |
||||
"Failed to provide a CAS service ticket to validate")); |
||||
} |
||||
|
||||
boolean stateless = (authentication instanceof UsernamePasswordAuthenticationToken |
||||
&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication.getPrincipal())); |
||||
CasAuthenticationToken result = null; |
||||
|
||||
if (stateless) { |
||||
// Try to obtain from cache
|
||||
result = this.statelessTicketCache.getByTicketId(authentication.getCredentials().toString()); |
||||
} |
||||
if (result == null) { |
||||
result = this.authenticateNow(authentication); |
||||
result.setDetails(authentication.getDetails()); |
||||
} |
||||
if (stateless) { |
||||
// Add to cache
|
||||
this.statelessTicketCache.putTicketInCache(result); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException { |
||||
try { |
||||
Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), |
||||
getServiceUrl(authentication)); |
||||
UserDetails userDetails = loadUserByAssertion(assertion); |
||||
this.userDetailsChecker.check(userDetails); |
||||
return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(), |
||||
this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion); |
||||
} |
||||
catch (TicketValidationException ex) { |
||||
throw new BadCredentialsException(ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Gets the serviceUrl. If the {@link Authentication#getDetails()} is an instance of |
||||
* {@link ServiceAuthenticationDetails}, then |
||||
* {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise, the |
||||
* {@link ServiceProperties#getService()} is used. |
||||
* @param authentication |
||||
* @return |
||||
*/ |
||||
private String getServiceUrl(Authentication authentication) { |
||||
String serviceUrl; |
||||
if (authentication.getDetails() instanceof ServiceAuthenticationDetails) { |
||||
return ((ServiceAuthenticationDetails) authentication.getDetails()).getServiceUrl(); |
||||
} |
||||
Assert.state(this.serviceProperties != null, |
||||
"serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails."); |
||||
Assert.state(this.serviceProperties.getService() != null, |
||||
"serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails."); |
||||
serviceUrl = this.serviceProperties.getService(); |
||||
logger.debug(LogMessage.format("serviceUrl = %s", serviceUrl)); |
||||
return serviceUrl; |
||||
} |
||||
|
||||
/** |
||||
* Template method for retrieving the UserDetails based on the assertion. Default is |
||||
* to call configured userDetailsService and pass the username. Deployers can override |
||||
* this method and retrieve the user based on any criteria they desire. |
||||
* @param assertion The CAS Assertion. |
||||
* @return the UserDetails. |
||||
*/ |
||||
protected UserDetails loadUserByAssertion(final Assertion assertion) { |
||||
final CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, ""); |
||||
return this.authenticationUserDetailsService.loadUserDetails(token); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
/** |
||||
* Sets the UserDetailsService to use. This is a convenience method to invoke |
||||
*/ |
||||
public void setUserDetailsService(final UserDetailsService userDetailsService) { |
||||
this.authenticationUserDetailsService = new UserDetailsByNameServiceWrapper(userDetailsService); |
||||
} |
||||
|
||||
public void setAuthenticationUserDetailsService( |
||||
final AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService) { |
||||
this.authenticationUserDetailsService = authenticationUserDetailsService; |
||||
} |
||||
|
||||
public void setServiceProperties(final ServiceProperties serviceProperties) { |
||||
this.serviceProperties = serviceProperties; |
||||
} |
||||
|
||||
protected String getKey() { |
||||
return this.key; |
||||
} |
||||
|
||||
public void setKey(String key) { |
||||
this.key = key; |
||||
} |
||||
|
||||
public StatelessTicketCache getStatelessTicketCache() { |
||||
return this.statelessTicketCache; |
||||
} |
||||
|
||||
protected TicketValidator getTicketValidator() { |
||||
return this.ticketValidator; |
||||
} |
||||
|
||||
@Override |
||||
public void setMessageSource(final MessageSource messageSource) { |
||||
this.messages = new MessageSourceAccessor(messageSource); |
||||
} |
||||
|
||||
public void setStatelessTicketCache(final StatelessTicketCache statelessTicketCache) { |
||||
this.statelessTicketCache = statelessTicketCache; |
||||
} |
||||
|
||||
public void setTicketValidator(final TicketValidator ticketValidator) { |
||||
this.ticketValidator = ticketValidator; |
||||
} |
||||
|
||||
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { |
||||
this.authoritiesMapper = authoritiesMapper; |
||||
} |
||||
|
||||
@Override |
||||
public boolean supports(final Class<?> authentication) { |
||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)) |
||||
|| (CasAuthenticationToken.class.isAssignableFrom(authentication)) |
||||
|| (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication)); |
||||
} |
||||
|
||||
} |
||||
@ -1,173 +0,0 @@
@@ -1,173 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import java.io.Serializable; |
||||
import java.util.Collection; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.SpringSecurityCoreVersion; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* Represents a successful CAS <code>Authentication</code>. |
||||
* |
||||
* @author Ben Alex |
||||
* @author Scott Battaglia |
||||
*/ |
||||
public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable { |
||||
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; |
||||
|
||||
private final Object credentials; |
||||
|
||||
private final Object principal; |
||||
|
||||
private final UserDetails userDetails; |
||||
|
||||
private final int keyHash; |
||||
|
||||
private final Assertion assertion; |
||||
|
||||
/** |
||||
* Constructor. |
||||
* @param key to identify if this object made by a given |
||||
* {@link CasAuthenticationProvider} |
||||
* @param principal typically the UserDetails object (cannot be <code>null</code>) |
||||
* @param credentials the service/proxy ticket ID from CAS (cannot be |
||||
* <code>null</code>) |
||||
* @param authorities the authorities granted to the user (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param userDetails the user details (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param assertion the assertion returned from the CAS servers. It contains the |
||||
* principal and how to obtain a proxy ticket for the user. |
||||
* @throws IllegalArgumentException if a <code>null</code> was passed |
||||
*/ |
||||
public CasAuthenticationToken(final String key, final Object principal, final Object credentials, |
||||
final Collection<? extends GrantedAuthority> authorities, final UserDetails userDetails, |
||||
final Assertion assertion) { |
||||
this(extractKeyHash(key), principal, credentials, authorities, userDetails, assertion); |
||||
} |
||||
|
||||
/** |
||||
* Private constructor for Jackson Deserialization support |
||||
* @param keyHash hashCode of provided key to identify if this object made by a given |
||||
* {@link CasAuthenticationProvider} |
||||
* @param principal typically the UserDetails object (cannot be <code>null</code>) |
||||
* @param credentials the service/proxy ticket ID from CAS (cannot be |
||||
* <code>null</code>) |
||||
* @param authorities the authorities granted to the user (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param userDetails the user details (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param assertion the assertion returned from the CAS servers. It contains the |
||||
* principal and how to obtain a proxy ticket for the user. |
||||
* @throws IllegalArgumentException if a <code>null</code> was passed |
||||
* @since 4.2 |
||||
*/ |
||||
private CasAuthenticationToken(final Integer keyHash, final Object principal, final Object credentials, |
||||
final Collection<? extends GrantedAuthority> authorities, final UserDetails userDetails, |
||||
final Assertion assertion) { |
||||
super(authorities); |
||||
if ((principal == null) || "".equals(principal) || (credentials == null) || "".equals(credentials) |
||||
|| (authorities == null) || (userDetails == null) || (assertion == null)) { |
||||
throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); |
||||
} |
||||
this.keyHash = keyHash; |
||||
this.principal = principal; |
||||
this.credentials = credentials; |
||||
this.userDetails = userDetails; |
||||
this.assertion = assertion; |
||||
setAuthenticated(true); |
||||
} |
||||
|
||||
private static Integer extractKeyHash(String key) { |
||||
Assert.hasLength(key, "key cannot be null or empty"); |
||||
return key.hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(final Object obj) { |
||||
if (!super.equals(obj)) { |
||||
return false; |
||||
} |
||||
if (obj instanceof CasAuthenticationToken) { |
||||
CasAuthenticationToken test = (CasAuthenticationToken) obj; |
||||
if (!this.assertion.equals(test.getAssertion())) { |
||||
return false; |
||||
} |
||||
if (this.getKeyHash() != test.getKeyHash()) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
int result = super.hashCode(); |
||||
result = 31 * result + this.credentials.hashCode(); |
||||
result = 31 * result + this.principal.hashCode(); |
||||
result = 31 * result + this.userDetails.hashCode(); |
||||
result = 31 * result + this.keyHash; |
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(this.assertion); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return this.credentials; |
||||
} |
||||
|
||||
public int getKeyHash() { |
||||
return this.keyHash; |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.principal; |
||||
} |
||||
|
||||
public Assertion getAssertion() { |
||||
return this.assertion; |
||||
} |
||||
|
||||
public UserDetails getUserDetails() { |
||||
return this.userDetails; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append(super.toString()); |
||||
sb.append(" Assertion: ").append(this.assertion); |
||||
sb.append(" Credentials (Service/Proxy Ticket): ").append(this.credentials); |
||||
return (sb.toString()); |
||||
} |
||||
|
||||
} |
||||
@ -1,64 +0,0 @@
@@ -1,64 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
/** |
||||
* Implementation of @link {@link StatelessTicketCache} that has no backing cache. Useful |
||||
* in instances where storing of tickets for stateless session management is not required. |
||||
* <p> |
||||
* This is the default StatelessTicketCache of the @link {@link CasAuthenticationProvider} |
||||
* to eliminate the unnecessary dependency on EhCache that applications have even if they |
||||
* are not using the stateless session management. |
||||
* |
||||
* @author Scott Battaglia |
||||
* @see CasAuthenticationProvider |
||||
*/ |
||||
public final class NullStatelessTicketCache implements StatelessTicketCache { |
||||
|
||||
/** |
||||
* @return null since we are not storing any tickets. |
||||
*/ |
||||
@Override |
||||
public CasAuthenticationToken getByTicketId(final String serviceTicket) { |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* This is a no-op since we are not storing tickets. |
||||
*/ |
||||
@Override |
||||
public void putTicketInCache(final CasAuthenticationToken token) { |
||||
// nothing to do
|
||||
} |
||||
|
||||
/** |
||||
* This is a no-op since we are not storing tickets. |
||||
*/ |
||||
@Override |
||||
public void removeTicketFromCache(final CasAuthenticationToken token) { |
||||
// nothing to do
|
||||
} |
||||
|
||||
/** |
||||
* This is a no-op since we are not storing tickets. |
||||
*/ |
||||
@Override |
||||
public void removeTicketFromCache(final String serviceTicket) { |
||||
// nothing to do
|
||||
} |
||||
|
||||
} |
||||
@ -1,69 +0,0 @@
@@ -1,69 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2013 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.cache.Cache; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Caches tickets using a Spring IoC defined {@link Cache}. |
||||
* |
||||
* @author Marten Deinum |
||||
* @since 3.2 |
||||
* |
||||
*/ |
||||
public class SpringCacheBasedTicketCache implements StatelessTicketCache { |
||||
|
||||
private static final Log logger = LogFactory.getLog(SpringCacheBasedTicketCache.class); |
||||
|
||||
private final Cache cache; |
||||
|
||||
public SpringCacheBasedTicketCache(Cache cache) { |
||||
Assert.notNull(cache, "cache mandatory"); |
||||
this.cache = cache; |
||||
} |
||||
|
||||
@Override |
||||
public CasAuthenticationToken getByTicketId(final String serviceTicket) { |
||||
final Cache.ValueWrapper element = (serviceTicket != null) ? this.cache.get(serviceTicket) : null; |
||||
logger.debug(LogMessage.of(() -> "Cache hit: " + (element != null) + "; service ticket: " + serviceTicket)); |
||||
return (element != null) ? (CasAuthenticationToken) element.get() : null; |
||||
} |
||||
|
||||
@Override |
||||
public void putTicketInCache(final CasAuthenticationToken token) { |
||||
String key = token.getCredentials().toString(); |
||||
logger.debug(LogMessage.of(() -> "Cache put: " + key)); |
||||
this.cache.put(key, token); |
||||
} |
||||
|
||||
@Override |
||||
public void removeTicketFromCache(final CasAuthenticationToken token) { |
||||
logger.debug(LogMessage.of(() -> "Cache remove: " + token.getCredentials().toString())); |
||||
this.removeTicketFromCache(token.getCredentials().toString()); |
||||
} |
||||
|
||||
@Override |
||||
public void removeTicketFromCache(final String serviceTicket) { |
||||
this.cache.evict(serviceTicket); |
||||
} |
||||
|
||||
} |
||||
@ -1,110 +0,0 @@
@@ -1,110 +0,0 @@
|
||||
/* |
||||
* Copyright 2004 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
/** |
||||
* Caches CAS service tickets and CAS proxy tickets for stateless connections. |
||||
* |
||||
* <p> |
||||
* When a service ticket or proxy ticket is validated against the CAS server, it is unable |
||||
* to be used again. Most types of callers are stateful and are associated with a given |
||||
* <code>HttpSession</code>. This allows the affirmative CAS validation outcome to be |
||||
* stored in the <code>HttpSession</code>, meaning the removal of the ticket from the CAS |
||||
* server is not an issue. |
||||
* </p> |
||||
* |
||||
* <P> |
||||
* Stateless callers, such as remoting protocols, cannot take advantage of |
||||
* <code>HttpSession</code>. If the stateless caller is located a significant network |
||||
* distance from the CAS server, acquiring a fresh service ticket or proxy ticket for each |
||||
* invocation would be expensive. |
||||
* </p> |
||||
* |
||||
* <P> |
||||
* To avoid this issue with stateless callers, it is expected stateless callers will |
||||
* obtain a single service ticket or proxy ticket, and then present this same ticket to |
||||
* the Spring Security secured application on each occasion. As no |
||||
* <code>HttpSession</code> is available for such callers, the affirmative CAS validation |
||||
* outcome cannot be stored in this location. |
||||
* </p> |
||||
* |
||||
* <P> |
||||
* The <code>StatelessTicketCache</code> enables the service tickets and proxy tickets |
||||
* belonging to stateless callers to be placed in a cache. This in-memory cache stores the |
||||
* <code>CasAuthenticationToken</code>, effectively providing the same capability as a |
||||
* <code>HttpSession</code> with the ticket identifier being the key rather than a session |
||||
* identifier. |
||||
* </p> |
||||
* |
||||
* <P> |
||||
* Implementations should provide a reasonable timeout on stored entries, such that the |
||||
* stateless caller are not required to unnecessarily acquire fresh CAS service tickets or |
||||
* proxy tickets. |
||||
* </p> |
||||
* |
||||
* @author Ben Alex |
||||
*/ |
||||
public interface StatelessTicketCache { |
||||
|
||||
/** |
||||
* Retrieves the <code>CasAuthenticationToken</code> associated with the specified |
||||
* ticket. |
||||
* |
||||
* <P> |
||||
* If not found, returns a <code>null</code><code>CasAuthenticationToken</code>. |
||||
* </p> |
||||
* @return the fully populated authentication token |
||||
*/ |
||||
CasAuthenticationToken getByTicketId(String serviceTicket); |
||||
|
||||
/** |
||||
* Adds the specified <code>CasAuthenticationToken</code> to the cache. |
||||
* |
||||
* <P> |
||||
* The {@link CasAuthenticationToken#getCredentials()} method is used to retrieve the |
||||
* service ticket number. |
||||
* </p> |
||||
* @param token to be added to the cache |
||||
*/ |
||||
void putTicketInCache(CasAuthenticationToken token); |
||||
|
||||
/** |
||||
* Removes the specified ticket from the cache, as per |
||||
* {@link #removeTicketFromCache(String)}. |
||||
* |
||||
* <P> |
||||
* Implementations should use {@link CasAuthenticationToken#getCredentials()} to |
||||
* obtain the ticket and then delegate to the {@link #removeTicketFromCache(String)} |
||||
* method. |
||||
* </p> |
||||
* @param token to be removed |
||||
*/ |
||||
void removeTicketFromCache(CasAuthenticationToken token); |
||||
|
||||
/** |
||||
* Removes the specified ticket from the cache, meaning that future calls will require |
||||
* a new service ticket. |
||||
* |
||||
* <P> |
||||
* This is in case applications wish to provide a session termination capability for |
||||
* their stateless clients. |
||||
* </p> |
||||
* @param serviceTicket to be removed |
||||
*/ |
||||
void removeTicketFromCache(String serviceTicket); |
||||
|
||||
} |
||||
@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* An {@code AuthenticationProvider} that can process CAS service tickets and proxy |
||||
* tickets. |
||||
*/ |
||||
package org.springframework.security.cas.authentication; |
||||
@ -1,69 +0,0 @@
@@ -1,69 +0,0 @@
|
||||
/* |
||||
* Copyright 2015-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.jackson2; |
||||
|
||||
import java.util.Date; |
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect; |
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
import org.jasig.cas.client.authentication.AttributePrincipal; |
||||
|
||||
/** |
||||
* Helps in jackson deserialization of class
|
||||
* {@link org.jasig.cas.client.validation.AssertionImpl}, which is used with |
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. To use |
||||
* this class we need to register with |
||||
* {@link com.fasterxml.jackson.databind.ObjectMapper}. Type information will be stored |
||||
* in @class property. |
||||
* <p> |
||||
* <pre> |
||||
* ObjectMapper mapper = new ObjectMapper(); |
||||
* mapper.registerModule(new CasJackson2Module()); |
||||
* </pre> |
||||
* |
||||
* @author Jitendra Singh |
||||
* @since 4.2 |
||||
* @see CasJackson2Module |
||||
* @see org.springframework.security.jackson2.SecurityJackson2Modules |
||||
*/ |
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) |
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, |
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE) |
||||
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
class AssertionImplMixin { |
||||
|
||||
/** |
||||
* Mixin Constructor helps in deserialize |
||||
* {@link org.jasig.cas.client.validation.AssertionImpl} |
||||
* @param principal the Principal to associate with the Assertion. |
||||
* @param validFromDate when the assertion is valid from. |
||||
* @param validUntilDate when the assertion is valid to. |
||||
* @param authenticationDate when the assertion is authenticated. |
||||
* @param attributes the key/value pairs for this attribute. |
||||
*/ |
||||
@JsonCreator |
||||
AssertionImplMixin(@JsonProperty("principal") AttributePrincipal principal, |
||||
@JsonProperty("validFromDate") Date validFromDate, @JsonProperty("validUntilDate") Date validUntilDate, |
||||
@JsonProperty("authenticationDate") Date authenticationDate, |
||||
@JsonProperty("attributes") Map<String, Object> attributes) { |
||||
} |
||||
|
||||
} |
||||
@ -1,66 +0,0 @@
@@ -1,66 +0,0 @@
|
||||
/* |
||||
* Copyright 2015-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.jackson2; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect; |
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
import org.jasig.cas.client.proxy.ProxyRetriever; |
||||
|
||||
/** |
||||
* Helps in deserialize {@link org.jasig.cas.client.authentication.AttributePrincipalImpl} |
||||
* which is used with |
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. Type |
||||
* information will be stored in property named @class. |
||||
* <p> |
||||
* <pre> |
||||
* ObjectMapper mapper = new ObjectMapper(); |
||||
* mapper.registerModule(new CasJackson2Module()); |
||||
* </pre> |
||||
* |
||||
* @author Jitendra Singh |
||||
* @since 4.2 |
||||
* @see CasJackson2Module |
||||
* @see org.springframework.security.jackson2.SecurityJackson2Modules |
||||
*/ |
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) |
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, |
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE) |
||||
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
class AttributePrincipalImplMixin { |
||||
|
||||
/** |
||||
* Mixin Constructor helps in deserialize |
||||
* {@link org.jasig.cas.client.authentication.AttributePrincipalImpl} |
||||
* @param name the unique identifier for the principal. |
||||
* @param attributes the key/value pairs for this principal. |
||||
* @param proxyGrantingTicket the ticket associated with this principal. |
||||
* @param proxyRetriever the ProxyRetriever implementation to call back to the CAS |
||||
* server. |
||||
*/ |
||||
@JsonCreator |
||||
AttributePrincipalImplMixin(@JsonProperty("name") String name, |
||||
@JsonProperty("attributes") Map<String, Object> attributes, |
||||
@JsonProperty("proxyGrantingTicket") String proxyGrantingTicket, |
||||
@JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever) { |
||||
} |
||||
|
||||
} |
||||
@ -1,83 +0,0 @@
@@ -1,83 +0,0 @@
|
||||
/* |
||||
* Copyright 2015-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.jackson2; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect; |
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
import org.jasig.cas.client.validation.Assertion; |
||||
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationProvider; |
||||
import org.springframework.security.cas.authentication.CasAuthenticationToken; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
|
||||
/** |
||||
* Mixin class which helps in deserialize |
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationToken} using |
||||
* jackson. Two more dependent classes needs to register along with this mixin class. |
||||
* <ol> |
||||
* <li>{@link org.springframework.security.cas.jackson2.AssertionImplMixin}</li> |
||||
* <li>{@link org.springframework.security.cas.jackson2.AttributePrincipalImplMixin}</li> |
||||
* </ol> |
||||
* |
||||
* <p> |
||||
* |
||||
* <pre> |
||||
* ObjectMapper mapper = new ObjectMapper(); |
||||
* mapper.registerModule(new CasJackson2Module()); |
||||
* </pre> |
||||
* |
||||
* @author Jitendra Singh |
||||
* @since 4.2 |
||||
* @see CasJackson2Module |
||||
* @see org.springframework.security.jackson2.SecurityJackson2Modules |
||||
*/ |
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) |
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE, |
||||
getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) |
||||
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
class CasAuthenticationTokenMixin { |
||||
|
||||
/** |
||||
* Mixin Constructor helps in deserialize {@link CasAuthenticationToken} |
||||
* @param keyHash hashCode of provided key to identify if this object made by a given |
||||
* {@link CasAuthenticationProvider} |
||||
* @param principal typically the UserDetails object (cannot be <code>null</code>) |
||||
* @param credentials the service/proxy ticket ID from CAS (cannot be |
||||
* <code>null</code>) |
||||
* @param authorities the authorities granted to the user (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param userDetails the user details (from the |
||||
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot |
||||
* be <code>null</code>) |
||||
* @param assertion the assertion returned from the CAS servers. It contains the |
||||
* principal and how to obtain a proxy ticket for the user. |
||||
*/ |
||||
@JsonCreator |
||||
CasAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal, |
||||
@JsonProperty("credentials") Object credentials, |
||||
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities, |
||||
@JsonProperty("userDetails") UserDetails userDetails, @JsonProperty("assertion") Assertion assertion) { |
||||
} |
||||
|
||||
} |
||||
@ -1,58 +0,0 @@
@@ -1,58 +0,0 @@
|
||||
/* |
||||
* Copyright 2015-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.jackson2; |
||||
|
||||
import com.fasterxml.jackson.core.Version; |
||||
import com.fasterxml.jackson.databind.module.SimpleModule; |
||||
import org.jasig.cas.client.authentication.AttributePrincipalImpl; |
||||
import org.jasig.cas.client.validation.AssertionImpl; |
||||
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationToken; |
||||
import org.springframework.security.jackson2.SecurityJackson2Modules; |
||||
|
||||
/** |
||||
* Jackson module for spring-security-cas. This module register |
||||
* {@link AssertionImplMixin}, {@link AttributePrincipalImplMixin} and |
||||
* {@link CasAuthenticationTokenMixin}. If no default typing enabled by default then it'll |
||||
* enable it because typing info is needed to properly serialize/deserialize objects. In |
||||
* order to use this module just add this module into your ObjectMapper configuration. |
||||
* |
||||
* <pre> |
||||
* ObjectMapper mapper = new ObjectMapper(); |
||||
* mapper.registerModule(new CasJackson2Module()); |
||||
* </pre> <b>Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list |
||||
* of all security modules on the classpath.</b> |
||||
* |
||||
* @author Jitendra Singh. |
||||
* @since 4.2 |
||||
* @see org.springframework.security.jackson2.SecurityJackson2Modules |
||||
*/ |
||||
public class CasJackson2Module extends SimpleModule { |
||||
|
||||
public CasJackson2Module() { |
||||
super(CasJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); |
||||
} |
||||
|
||||
@Override |
||||
public void setupModule(SetupContext context) { |
||||
SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); |
||||
context.setMixInAnnotations(AssertionImpl.class, AssertionImplMixin.class); |
||||
context.setMixInAnnotations(AttributePrincipalImpl.class, AttributePrincipalImplMixin.class); |
||||
context.setMixInAnnotations(CasAuthenticationToken.class, CasAuthenticationTokenMixin.class); |
||||
} |
||||
|
||||
} |
||||
@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Spring Security support for Jasig's Central Authentication Service |
||||
* (<a href="https://www.jasig.org/cas">CAS</a>). |
||||
*/ |
||||
package org.springframework.security.cas; |
||||
@ -1,51 +0,0 @@
@@ -1,51 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.userdetails; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
|
||||
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; |
||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
|
||||
/** |
||||
* Abstract class for using the provided CAS assertion to construct a new User object. |
||||
* This generally is most useful when combined with a SAML-based response from the CAS |
||||
* Server/client. |
||||
* |
||||
* @author Scott Battaglia |
||||
* @since 3.0 |
||||
*/ |
||||
public abstract class AbstractCasAssertionUserDetailsService |
||||
implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> { |
||||
|
||||
@Override |
||||
public final UserDetails loadUserDetails(final CasAssertionAuthenticationToken token) { |
||||
return loadUserDetails(token.getAssertion()); |
||||
} |
||||
|
||||
/** |
||||
* Protected template method for construct a |
||||
* {@link org.springframework.security.core.userdetails.UserDetails} via the supplied |
||||
* CAS assertion. |
||||
* @param assertion the assertion to use to construct the new UserDetails. CANNOT be |
||||
* NULL. |
||||
* @return the newly constructed UserDetails. |
||||
*/ |
||||
protected abstract UserDetails loadUserDetails(Assertion assertion); |
||||
|
||||
} |
||||
@ -1,87 +0,0 @@
@@ -1,87 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.userdetails; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
|
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.userdetails.User; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Populates the {@link org.springframework.security.core.GrantedAuthority}s for a user by |
||||
* reading a list of attributes that were returned as part of the CAS response. Each |
||||
* attribute is read and each value of the attribute is turned into a GrantedAuthority. If |
||||
* the attribute has no value then its not added. |
||||
* |
||||
* @author Scott Battaglia |
||||
* @since 3.0 |
||||
*/ |
||||
public final class GrantedAuthorityFromAssertionAttributesUserDetailsService |
||||
extends AbstractCasAssertionUserDetailsService { |
||||
|
||||
private static final String NON_EXISTENT_PASSWORD_VALUE = "NO_PASSWORD"; |
||||
|
||||
private final String[] attributes; |
||||
|
||||
private boolean convertToUpperCase = true; |
||||
|
||||
public GrantedAuthorityFromAssertionAttributesUserDetailsService(final String[] attributes) { |
||||
Assert.notNull(attributes, "attributes cannot be null."); |
||||
Assert.isTrue(attributes.length > 0, "At least one attribute is required to retrieve roles from."); |
||||
this.attributes = attributes; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
@Override |
||||
protected UserDetails loadUserDetails(final Assertion assertion) { |
||||
List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); |
||||
for (String attribute : this.attributes) { |
||||
Object value = assertion.getPrincipal().getAttributes().get(attribute); |
||||
if (value != null) { |
||||
if (value instanceof List) { |
||||
for (Object o : (List<?>) value) { |
||||
grantedAuthorities.add(createSimpleGrantedAuthority(o)); |
||||
} |
||||
} |
||||
else { |
||||
grantedAuthorities.add(createSimpleGrantedAuthority(value)); |
||||
} |
||||
} |
||||
} |
||||
return new User(assertion.getPrincipal().getName(), NON_EXISTENT_PASSWORD_VALUE, true, true, true, true, |
||||
grantedAuthorities); |
||||
} |
||||
|
||||
private SimpleGrantedAuthority createSimpleGrantedAuthority(Object o) { |
||||
return new SimpleGrantedAuthority(this.convertToUpperCase ? o.toString().toUpperCase() : o.toString()); |
||||
} |
||||
|
||||
/** |
||||
* Converts the returned attribute values to uppercase values. |
||||
* @param convertToUpperCase true if it should convert, false otherwise. |
||||
*/ |
||||
public void setConvertToUpperCase(final boolean convertToUpperCase) { |
||||
this.convertToUpperCase = convertToUpperCase; |
||||
} |
||||
|
||||
} |
||||
@ -1,150 +0,0 @@
@@ -1,150 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.jasig.cas.client.util.CommonUtils; |
||||
|
||||
import org.springframework.beans.factory.InitializingBean; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.web.AuthenticationEntryPoint; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Used by the <code>ExceptionTranslationFilter</code> to commence authentication via the |
||||
* JA-SIG Central Authentication Service (CAS). |
||||
* <p> |
||||
* The user's browser will be redirected to the JA-SIG CAS enterprise-wide login page. |
||||
* This page is specified by the <code>loginUrl</code> property. Once login is complete, |
||||
* the CAS login page will redirect to the page indicated by the <code>service</code> |
||||
* property. The <code>service</code> is a HTTP URL belonging to the current application. |
||||
* The <code>service</code> URL is monitored by the {@link CasAuthenticationFilter}, which |
||||
* will validate the CAS login was successful. |
||||
* |
||||
* @author Ben Alex |
||||
* @author Scott Battaglia |
||||
*/ |
||||
public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean { |
||||
|
||||
private ServiceProperties serviceProperties; |
||||
|
||||
private String loginUrl; |
||||
|
||||
/** |
||||
* Determines whether the Service URL should include the session id for the specific |
||||
* user. As of CAS 3.0.5, the session id will automatically be stripped. However, |
||||
* older versions of CAS (i.e. CAS 2), do not automatically strip the session |
||||
* identifier (this is a bug on the part of the older server implementations), so an |
||||
* option to disable the session encoding is provided for backwards compatibility. |
||||
* |
||||
* By default, encoding is enabled. |
||||
*/ |
||||
private boolean encodeServiceUrlWithSessionId = true; |
||||
|
||||
@Override |
||||
public void afterPropertiesSet() { |
||||
Assert.hasLength(this.loginUrl, "loginUrl must be specified"); |
||||
Assert.notNull(this.serviceProperties, "serviceProperties must be specified"); |
||||
Assert.notNull(this.serviceProperties.getService(), "serviceProperties.getService() cannot be null."); |
||||
} |
||||
|
||||
@Override |
||||
public final void commence(final HttpServletRequest servletRequest, HttpServletResponse response, |
||||
AuthenticationException authenticationException) throws IOException { |
||||
String urlEncodedService = createServiceUrl(servletRequest, response); |
||||
String redirectUrl = createRedirectUrl(urlEncodedService); |
||||
preCommence(servletRequest, response); |
||||
response.sendRedirect(redirectUrl); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a new Service Url. The default implementation relies on the CAS client |
||||
* to do the bulk of the work. |
||||
* @param request the HttpServletRequest |
||||
* @param response the HttpServlet Response |
||||
* @return the constructed service url. CANNOT be NULL. |
||||
*/ |
||||
protected String createServiceUrl(HttpServletRequest request, HttpServletResponse response) { |
||||
return CommonUtils.constructServiceUrl(null, response, this.serviceProperties.getService(), null, |
||||
this.serviceProperties.getArtifactParameter(), this.encodeServiceUrlWithSessionId); |
||||
} |
||||
|
||||
/** |
||||
* Constructs the Url for Redirection to the CAS server. Default implementation relies |
||||
* on the CAS client to do the bulk of the work. |
||||
* @param serviceUrl the service url that should be included. |
||||
* @return the redirect url. CANNOT be NULL. |
||||
*/ |
||||
protected String createRedirectUrl(String serviceUrl) { |
||||
return CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, |
||||
this.serviceProperties.isSendRenew(), false); |
||||
} |
||||
|
||||
/** |
||||
* Template method for you to do your own pre-processing before the redirect occurs. |
||||
* @param request the HttpServletRequest |
||||
* @param response the HttpServletResponse |
||||
*/ |
||||
protected void preCommence(HttpServletRequest request, HttpServletResponse response) { |
||||
|
||||
} |
||||
|
||||
/** |
||||
* The enterprise-wide CAS login URL. Usually something like |
||||
* <code>https://www.mycompany.com/cas/login</code>.
|
||||
* @return the enterprise-wide CAS login URL |
||||
*/ |
||||
public final String getLoginUrl() { |
||||
return this.loginUrl; |
||||
} |
||||
|
||||
public final ServiceProperties getServiceProperties() { |
||||
return this.serviceProperties; |
||||
} |
||||
|
||||
public final void setLoginUrl(String loginUrl) { |
||||
this.loginUrl = loginUrl; |
||||
} |
||||
|
||||
public final void setServiceProperties(ServiceProperties serviceProperties) { |
||||
this.serviceProperties = serviceProperties; |
||||
} |
||||
|
||||
/** |
||||
* Sets whether to encode the service url with the session id or not. |
||||
* @param encodeServiceUrlWithSessionId whether to encode the service url with the |
||||
* session id or not. |
||||
*/ |
||||
public final void setEncodeServiceUrlWithSessionId(boolean encodeServiceUrlWithSessionId) { |
||||
this.encodeServiceUrlWithSessionId = encodeServiceUrlWithSessionId; |
||||
} |
||||
|
||||
/** |
||||
* Sets whether to encode the service url with the session id or not. |
||||
* @return whether to encode the service url with the session id or not. |
||||
* |
||||
*/ |
||||
protected boolean getEncodeServiceUrlWithSessionId() { |
||||
return this.encodeServiceUrlWithSessionId; |
||||
} |
||||
|
||||
} |
||||
@ -1,397 +0,0 @@
@@ -1,397 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; |
||||
import org.jasig.cas.client.util.CommonUtils; |
||||
import org.jasig.cas.client.validation.TicketValidator; |
||||
|
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken; |
||||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; |
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.context.SecurityContext; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy |
||||
* tickets. |
||||
* <h2>Service Tickets</h2> |
||||
* <p> |
||||
* A service ticket consists of an opaque ticket string. It arrives at this filter by the |
||||
* user's browser successfully authenticating using CAS, and then receiving a HTTP |
||||
* redirect to a <code>service</code>. The opaque ticket string is presented in the |
||||
* <code>ticket</code> request parameter. |
||||
* <p> |
||||
* This filter monitors the <code>service</code> URL so it can receive the service ticket |
||||
* and process it. By default this filter processes the URL <tt>/login/cas</tt>. When |
||||
* processing this URL, the value of {@link ServiceProperties#getService()} is used as the |
||||
* <tt>service</tt> when validating the <code>ticket</code>. This means that it is |
||||
* important that {@link ServiceProperties#getService()} specifies the same value as the |
||||
* <tt>filterProcessesUrl</tt>. |
||||
* <p> |
||||
* Processing the service ticket involves creating a |
||||
* <code>UsernamePasswordAuthenticationToken</code> which uses |
||||
* {@link #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque ticket |
||||
* string as the <code>credentials</code>. |
||||
* <h2>Obtaining Proxy Granting Tickets</h2> |
||||
* <p> |
||||
* If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter |
||||
* will respond to requests matching this url so that the CAS Server can provide a PGT to |
||||
* the filter. Note that in addition to the <code>proxyReceptorUrl</code> a non-null |
||||
* <code>proxyGrantingTicketStorage</code> must be provided in order for the filter to |
||||
* respond to proxy receptor requests. By configuring a shared |
||||
* {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the |
||||
* CasAuthenticationFilter one can have the CasAuthenticationFilter handle the proxying |
||||
* requirements for CAS. |
||||
* <h2>Proxy Tickets</h2> |
||||
* <p> |
||||
* The filter can process tickets present on any url. This is useful when wanting to |
||||
* process proxy tickets. In order for proxy tickets to get processed |
||||
* {@link ServiceProperties#isAuthenticateAllArtifacts()} must return <code>true</code>. |
||||
* Additionally, if the request is already authenticated, authentication will <b>not</b> |
||||
* occur. Last, {@link AuthenticationDetailsSource#buildDetails(Object)} must return a |
||||
* {@link ServiceAuthenticationDetails}. This can be accomplished using the |
||||
* {@link ServiceAuthenticationDetailsSource}. In this case |
||||
* {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url. |
||||
* <p> |
||||
* Processing the proxy ticket involves creating a |
||||
* <code>UsernamePasswordAuthenticationToken</code> which uses |
||||
* {@link #CAS_STATELESS_IDENTIFIER} for the <code>principal</code> and the opaque ticket |
||||
* string as the <code>credentials</code>. When a proxy ticket is successfully |
||||
* authenticated, the FilterChain continues and the |
||||
* <code>authenticationSuccessHandler</code> is not used. |
||||
* <h2>Notes about the <code>AuthenticationManager</code></h2> |
||||
* <p> |
||||
* The configured <code>AuthenticationManager</code> is expected to provide a provider |
||||
* that can recognise <code>UsernamePasswordAuthenticationToken</code>s containing this |
||||
* special <code>principal</code> name, and process them accordingly by validation with |
||||
* the CAS server. Additionally, it should be capable of using the result of |
||||
* {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the |
||||
* ticket. |
||||
* <h2>Example Configuration</h2> |
||||
* <p> |
||||
* An example configuration that supports service tickets, obtaining proxy granting |
||||
* tickets, and proxy tickets is illustrated below: |
||||
* |
||||
* <pre> |
||||
* <b:bean id="serviceProperties" |
||||
* class="org.springframework.security.cas.ServiceProperties" |
||||
* p:service="https://service.example.com/cas-sample/login/cas"
|
||||
* p:authenticateAllArtifacts="true"/> |
||||
* <b:bean id="casEntryPoint" |
||||
* class="org.springframework.security.cas.web.CasAuthenticationEntryPoint" |
||||
* p:serviceProperties-ref="serviceProperties" p:loginUrl="https://login.example.org/cas/login" />
|
||||
* <b:bean id="casFilter" |
||||
* class="org.springframework.security.cas.web.CasAuthenticationFilter" |
||||
* p:authenticationManager-ref="authManager" |
||||
* p:serviceProperties-ref="serviceProperties" |
||||
* p:proxyGrantingTicketStorage-ref="pgtStorage" |
||||
* p:proxyReceptorUrl="/login/cas/proxyreceptor"> |
||||
* <b:property name="authenticationDetailsSource"> |
||||
* <b:bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/> |
||||
* </b:property> |
||||
* <b:property name="authenticationFailureHandler"> |
||||
* <b:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler" |
||||
* p:defaultFailureUrl="/casfailed.jsp"/> |
||||
* </b:property> |
||||
* </b:bean> |
||||
* <!-- |
||||
* NOTE: In a real application you should not use an in memory implementation. You will also want |
||||
* to ensure to clean up expired tickets by calling ProxyGrantingTicketStorage.cleanup() |
||||
* --> |
||||
* <b:bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/> |
||||
* <b:bean id="casAuthProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider" |
||||
* p:serviceProperties-ref="serviceProperties" |
||||
* p:key="casAuthProviderKey"> |
||||
* <b:property name="authenticationUserDetailsService"> |
||||
* <b:bean |
||||
* class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> |
||||
* <b:constructor-arg ref="userService" /> |
||||
* </b:bean> |
||||
* </b:property> |
||||
* <b:property name="ticketValidator"> |
||||
* <b:bean |
||||
* class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator" |
||||
* p:acceptAnyProxy="true" |
||||
* p:proxyCallbackUrl="https://service.example.com/cas-sample/login/cas/proxyreceptor"
|
||||
* p:proxyGrantingTicketStorage-ref="pgtStorage"> |
||||
* <b:constructor-arg value="https://login.example.org/cas" />
|
||||
* </b:bean> |
||||
* </b:property> |
||||
* <b:property name="statelessTicketCache"> |
||||
* <b:bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache"> |
||||
* <b:property name="cache"> |
||||
* <b:bean class="net.sf.ehcache.Cache" |
||||
* init-method="initialise" |
||||
* destroy-method="dispose"> |
||||
* <b:constructor-arg value="casTickets"/> |
||||
* <b:constructor-arg value="50"/> |
||||
* <b:constructor-arg value="true"/> |
||||
* <b:constructor-arg value="false"/> |
||||
* <b:constructor-arg value="3600"/> |
||||
* <b:constructor-arg value="900"/> |
||||
* </b:bean> |
||||
* </b:property> |
||||
* </b:bean> |
||||
* </b:property> |
||||
* </b:bean> |
||||
* </pre> |
||||
* |
||||
* @author Ben Alex |
||||
* @author Rob Winch |
||||
*/ |
||||
public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFilter { |
||||
|
||||
/** |
||||
* Used to identify a CAS request for a stateful user agent, such as a web browser. |
||||
*/ |
||||
public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_"; |
||||
|
||||
/** |
||||
* Used to identify a CAS request for a stateless user agent, such as a remoting |
||||
* protocol client (e.g. Hessian, Burlap, SOAP etc). Results in a more aggressive |
||||
* caching strategy being used, as the absence of a <code>HttpSession</code> will |
||||
* result in a new authentication attempt on every request. |
||||
*/ |
||||
public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_"; |
||||
|
||||
/** |
||||
* The last portion of the receptor url, i.e. /proxy/receptor |
||||
*/ |
||||
private RequestMatcher proxyReceptorMatcher; |
||||
|
||||
/** |
||||
* The backing storage to store ProxyGrantingTicket requests. |
||||
*/ |
||||
private ProxyGrantingTicketStorage proxyGrantingTicketStorage; |
||||
|
||||
private String artifactParameter = ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER; |
||||
|
||||
private boolean authenticateAllArtifacts; |
||||
|
||||
private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler(); |
||||
|
||||
public CasAuthenticationFilter() { |
||||
super("/login/cas"); |
||||
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); |
||||
} |
||||
|
||||
@Override |
||||
protected final void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, |
||||
FilterChain chain, Authentication authResult) throws IOException, ServletException { |
||||
boolean continueFilterChain = proxyTicketRequest(serviceTicketRequest(request, response), request); |
||||
if (!continueFilterChain) { |
||||
super.successfulAuthentication(request, response, chain, authResult); |
||||
return; |
||||
} |
||||
this.logger.debug( |
||||
LogMessage.format("Authentication success. Updating SecurityContextHolder to contain: %s", authResult)); |
||||
SecurityContext context = SecurityContextHolder.createEmptyContext(); |
||||
context.setAuthentication(authResult); |
||||
SecurityContextHolder.setContext(context); |
||||
if (this.eventPublisher != null) { |
||||
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); |
||||
} |
||||
chain.doFilter(request, response); |
||||
} |
||||
|
||||
@Override |
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
||||
throws AuthenticationException, IOException { |
||||
// if the request is a proxy request process it and return null to indicate the
|
||||
// request has been processed
|
||||
if (proxyReceptorRequest(request)) { |
||||
this.logger.debug("Responding to proxy receptor request"); |
||||
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage); |
||||
return null; |
||||
} |
||||
boolean serviceTicketRequest = serviceTicketRequest(request, response); |
||||
String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER : CAS_STATELESS_IDENTIFIER; |
||||
String password = obtainArtifact(request); |
||||
if (password == null) { |
||||
this.logger.debug("Failed to obtain an artifact (cas ticket)"); |
||||
password = ""; |
||||
} |
||||
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); |
||||
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); |
||||
return this.getAuthenticationManager().authenticate(authRequest); |
||||
} |
||||
|
||||
/** |
||||
* If present, gets the artifact (CAS ticket) from the {@link HttpServletRequest}. |
||||
* @param request |
||||
* @return if present the artifact from the {@link HttpServletRequest}, else null |
||||
*/ |
||||
protected String obtainArtifact(HttpServletRequest request) { |
||||
return request.getParameter(this.artifactParameter); |
||||
} |
||||
|
||||
/** |
||||
* Overridden to provide proxying capabilities. |
||||
*/ |
||||
@Override |
||||
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { |
||||
final boolean serviceTicketRequest = serviceTicketRequest(request, response); |
||||
final boolean result = serviceTicketRequest || proxyReceptorRequest(request) |
||||
|| (proxyTicketRequest(serviceTicketRequest, request)); |
||||
if (this.logger.isDebugEnabled()) { |
||||
this.logger.debug("requiresAuthentication = " + result); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationFailureHandler} for proxy requests. |
||||
* @param proxyFailureHandler |
||||
*/ |
||||
public final void setProxyAuthenticationFailureHandler(AuthenticationFailureHandler proxyFailureHandler) { |
||||
Assert.notNull(proxyFailureHandler, "proxyFailureHandler cannot be null"); |
||||
this.proxyFailureHandler = proxyFailureHandler; |
||||
} |
||||
|
||||
/** |
||||
* Wraps the {@link AuthenticationFailureHandler} to distinguish between handling |
||||
* proxy ticket authentication failures and service ticket failures. |
||||
*/ |
||||
@Override |
||||
public final void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { |
||||
super.setAuthenticationFailureHandler(new CasAuthenticationFailureHandler(failureHandler)); |
||||
} |
||||
|
||||
public final void setProxyReceptorUrl(final String proxyReceptorUrl) { |
||||
this.proxyReceptorMatcher = new AntPathRequestMatcher("/**" + proxyReceptorUrl); |
||||
} |
||||
|
||||
public final void setProxyGrantingTicketStorage(final ProxyGrantingTicketStorage proxyGrantingTicketStorage) { |
||||
this.proxyGrantingTicketStorage = proxyGrantingTicketStorage; |
||||
} |
||||
|
||||
public final void setServiceProperties(final ServiceProperties serviceProperties) { |
||||
this.artifactParameter = serviceProperties.getArtifactParameter(); |
||||
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts(); |
||||
} |
||||
|
||||
/** |
||||
* Indicates if the request is elgible to process a service ticket. This method exists |
||||
* for readability. |
||||
* @param request |
||||
* @param response |
||||
* @return |
||||
*/ |
||||
private boolean serviceTicketRequest(HttpServletRequest request, HttpServletResponse response) { |
||||
boolean result = super.requiresAuthentication(request, response); |
||||
this.logger.debug(LogMessage.format("serviceTicketRequest = %s", result)); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Indicates if the request is elgible to process a proxy ticket. |
||||
* @param request |
||||
* @return |
||||
*/ |
||||
private boolean proxyTicketRequest(boolean serviceTicketRequest, HttpServletRequest request) { |
||||
if (serviceTicketRequest) { |
||||
return false; |
||||
} |
||||
boolean result = this.authenticateAllArtifacts && obtainArtifact(request) != null && !authenticated(); |
||||
this.logger.debug(LogMessage.format("proxyTicketRequest = %s", result)); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Determines if a user is already authenticated. |
||||
* @return |
||||
*/ |
||||
private boolean authenticated() { |
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
||||
return authentication != null && authentication.isAuthenticated() |
||||
&& !(authentication instanceof AnonymousAuthenticationToken); |
||||
} |
||||
|
||||
/** |
||||
* Indicates if the request is elgible to be processed as the proxy receptor. |
||||
* @param request |
||||
* @return |
||||
*/ |
||||
private boolean proxyReceptorRequest(HttpServletRequest request) { |
||||
final boolean result = proxyReceptorConfigured() && this.proxyReceptorMatcher.matches(request); |
||||
this.logger.debug(LogMessage.format("proxyReceptorRequest = %s", result)); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Determines if the {@link CasAuthenticationFilter} is configured to handle the proxy |
||||
* receptor requests. |
||||
* @return |
||||
*/ |
||||
private boolean proxyReceptorConfigured() { |
||||
final boolean result = this.proxyGrantingTicketStorage != null && this.proxyReceptorMatcher != null; |
||||
this.logger.debug(LogMessage.format("proxyReceptorConfigured = %s", result)); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* A wrapper for the AuthenticationFailureHandler that will flex the |
||||
* {@link AuthenticationFailureHandler} that is used. The value |
||||
* {@link CasAuthenticationFilter#setProxyAuthenticationFailureHandler(AuthenticationFailureHandler)} |
||||
* will be used for proxy requests that fail. The value |
||||
* {@link CasAuthenticationFilter#setAuthenticationFailureHandler(AuthenticationFailureHandler)} |
||||
* will be used for service tickets that fail. |
||||
*/ |
||||
private class CasAuthenticationFailureHandler implements AuthenticationFailureHandler { |
||||
|
||||
private final AuthenticationFailureHandler serviceTicketFailureHandler; |
||||
|
||||
CasAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { |
||||
Assert.notNull(failureHandler, "failureHandler"); |
||||
this.serviceTicketFailureHandler = failureHandler; |
||||
} |
||||
|
||||
@Override |
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, |
||||
AuthenticationException exception) throws IOException, ServletException { |
||||
if (serviceTicketRequest(request, response)) { |
||||
this.serviceTicketFailureHandler.onAuthenticationFailure(request, response, exception); |
||||
} |
||||
else { |
||||
CasAuthenticationFilter.this.proxyFailureHandler.onAuthenticationFailure(request, response, exception); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,146 +0,0 @@
@@ -1,146 +0,0 @@
|
||||
/* |
||||
* Copyright 2011-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web.authentication; |
||||
|
||||
import java.net.MalformedURLException; |
||||
import java.net.URL; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails; |
||||
import org.springframework.security.web.util.UrlUtils; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A default implementation of {@link ServiceAuthenticationDetails} that figures out the |
||||
* value for {@link #getServiceUrl()} by inspecting the current {@link HttpServletRequest} |
||||
* and using the current URL minus the artifact and the corresponding value. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails |
||||
implements ServiceAuthenticationDetails { |
||||
|
||||
private static final long serialVersionUID = 6192409090610517700L; |
||||
|
||||
private final String serviceUrl; |
||||
|
||||
/** |
||||
* Creates a new instance |
||||
* @param request the current {@link HttpServletRequest} to obtain the |
||||
* {@link #getServiceUrl()} from. |
||||
* @param artifactPattern the {@link Pattern} that will be used to clean up the query |
||||
* string from containing the artifact name and value. This can be created using |
||||
* {@link #createArtifactPattern(String)}. |
||||
*/ |
||||
DefaultServiceAuthenticationDetails(String casService, HttpServletRequest request, Pattern artifactPattern) |
||||
throws MalformedURLException { |
||||
super(request); |
||||
URL casServiceUrl = new URL(casService); |
||||
int port = getServicePort(casServiceUrl); |
||||
final String query = getQueryString(request, artifactPattern); |
||||
this.serviceUrl = UrlUtils.buildFullRequestUrl(casServiceUrl.getProtocol(), casServiceUrl.getHost(), port, |
||||
request.getRequestURI(), query); |
||||
} |
||||
|
||||
/** |
||||
* Returns the current URL minus the artifact parameter and its value, if present. |
||||
* @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails#getServiceUrl() |
||||
*/ |
||||
@Override |
||||
public String getServiceUrl() { |
||||
return this.serviceUrl; |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (this == obj) { |
||||
return true; |
||||
} |
||||
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) { |
||||
return false; |
||||
} |
||||
ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj; |
||||
return this.serviceUrl.equals(that.getServiceUrl()); |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
final int prime = 31; |
||||
int result = super.hashCode(); |
||||
result = prime * result + this.serviceUrl.hashCode(); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
StringBuilder result = new StringBuilder(); |
||||
result.append(super.toString()); |
||||
result.append("ServiceUrl: "); |
||||
result.append(this.serviceUrl); |
||||
return result.toString(); |
||||
} |
||||
|
||||
/** |
||||
* If present, removes the artifactParameterName and the corresponding value from the |
||||
* query String. |
||||
* @param request |
||||
* @return the query String minus the artifactParameterName and the corresponding |
||||
* value. |
||||
*/ |
||||
private String getQueryString(final HttpServletRequest request, final Pattern artifactPattern) { |
||||
final String query = request.getQueryString(); |
||||
if (query == null) { |
||||
return null; |
||||
} |
||||
String result = artifactPattern.matcher(query).replaceFirst(""); |
||||
if (result.length() == 0) { |
||||
return null; |
||||
} |
||||
// strip off the trailing & only if the artifact was the first query param
|
||||
return result.startsWith("&") ? result.substring(1) : result; |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link Pattern} that can be passed into the constructor. This allows the |
||||
* {@link Pattern} to be reused for every instance of |
||||
* {@link DefaultServiceAuthenticationDetails}. |
||||
* @param artifactParameterName |
||||
* @return |
||||
*/ |
||||
static Pattern createArtifactPattern(String artifactParameterName) { |
||||
Assert.hasLength(artifactParameterName, "artifactParameterName is expected to have a length"); |
||||
return Pattern.compile("&?" + Pattern.quote(artifactParameterName) + "=[^&]*"); |
||||
} |
||||
|
||||
/** |
||||
* Gets the port from the casServiceURL ensuring to return the proper value if the |
||||
* default port is being used. |
||||
* @param casServiceUrl the casServerUrl to be used (i.e. |
||||
* "https://example.com/context/login/cas") |
||||
* @return the port that is configured for the casServerUrl |
||||
*/ |
||||
private static int getServicePort(URL casServiceUrl) { |
||||
int port = casServiceUrl.getPort(); |
||||
if (port == -1) { |
||||
port = casServiceUrl.getDefaultPort(); |
||||
} |
||||
return port; |
||||
} |
||||
|
||||
} |
||||
@ -1,42 +0,0 @@
@@ -1,42 +0,0 @@
|
||||
/* |
||||
* Copyright 2011-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web.authentication; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.cas.authentication.CasAuthenticationProvider; |
||||
import org.springframework.security.core.Authentication; |
||||
|
||||
/** |
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url |
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()} |
||||
* should implement this interface when tickets can be sent to any URL rather than only |
||||
* {@link ServiceProperties#getService()}. |
||||
* |
||||
* @author Rob Winch |
||||
* @see ServiceAuthenticationDetailsSource |
||||
*/ |
||||
public interface ServiceAuthenticationDetails extends Serializable { |
||||
|
||||
/** |
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
* @return the service url. Cannot be <code>null</code>. |
||||
*/ |
||||
String getServiceUrl(); |
||||
|
||||
} |
||||
@ -1,83 +0,0 @@
@@ -1,83 +0,0 @@
|
||||
/* |
||||
* Copyright 2011-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web.authentication; |
||||
|
||||
import java.net.MalformedURLException; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* The {@code AuthenticationDetailsSource} that is set on the |
||||
* {@code CasAuthenticationFilter} should return a value that implements |
||||
* {@code ServiceAuthenticationDetails} if the application needs to authenticate dynamic |
||||
* service urls. The |
||||
* {@code ServiceAuthenticationDetailsSource#buildDetails(HttpServletRequest)} creates a |
||||
* default {@code ServiceAuthenticationDetails}. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
public class ServiceAuthenticationDetailsSource |
||||
implements AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails> { |
||||
|
||||
private final Pattern artifactPattern; |
||||
|
||||
private ServiceProperties serviceProperties; |
||||
|
||||
/** |
||||
* Creates an implementation that uses the specified ServiceProperties and the default |
||||
* CAS artifactParameterName. |
||||
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl. |
||||
*/ |
||||
public ServiceAuthenticationDetailsSource(ServiceProperties serviceProperties) { |
||||
this(serviceProperties, ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER); |
||||
} |
||||
|
||||
/** |
||||
* Creates an implementation that uses the specified artifactParameterName |
||||
* @param serviceProperties The ServiceProperties to use to construct the serviceUrl. |
||||
* @param artifactParameterName the artifactParameterName that is removed from the |
||||
* current URL. The result becomes the service url. Cannot be null and cannot be an |
||||
* empty String. |
||||
*/ |
||||
public ServiceAuthenticationDetailsSource(ServiceProperties serviceProperties, String artifactParameterName) { |
||||
Assert.notNull(serviceProperties, "serviceProperties cannot be null"); |
||||
this.serviceProperties = serviceProperties; |
||||
this.artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(artifactParameterName); |
||||
} |
||||
|
||||
/** |
||||
* @param context the {@code HttpServletRequest} object. |
||||
* @return the {@code ServiceAuthenticationDetails} containing information about the |
||||
* current request |
||||
*/ |
||||
@Override |
||||
public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) { |
||||
try { |
||||
return new DefaultServiceAuthenticationDetails(this.serviceProperties.getService(), context, |
||||
this.artifactPattern); |
||||
} |
||||
catch (MalformedURLException ex) { |
||||
throw new RuntimeException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,21 +0,0 @@
@@ -1,21 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Authentication processing mechanisms which respond to the submission of authentication |
||||
* credentials using CAS. |
||||
*/ |
||||
package org.springframework.security.cas.web.authentication; |
||||
@ -1,20 +0,0 @@
@@ -1,20 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
/** |
||||
* Authenticates standard web browser users via CAS. |
||||
*/ |
||||
package org.springframework.security.cas.web; |
||||
@ -1,45 +0,0 @@
@@ -1,45 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.jasig.cas.client.validation.AssertionImpl; |
||||
|
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.userdetails.User; |
||||
|
||||
/** |
||||
* @author Scott Battaglia |
||||
* @since 2.0 |
||||
* |
||||
*/ |
||||
public abstract class AbstractStatelessTicketCacheTests { |
||||
|
||||
protected CasAuthenticationToken getToken() { |
||||
List<String> proxyList = new ArrayList<>(); |
||||
proxyList.add("https://localhost/newPortal/login/cas"); |
||||
User user = new User("rod", "password", true, true, true, true, |
||||
AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); |
||||
final Assertion assertion = new AssertionImpl("rod"); |
||||
return new CasAuthenticationToken("key", user, "ST-0-ER94xMJmn6pha35CQRoZ", |
||||
AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"), user, assertion); |
||||
} |
||||
|
||||
} |
||||
@ -1,382 +0,0 @@
@@ -1,382 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.jasig.cas.client.validation.AssertionImpl; |
||||
import org.jasig.cas.client.validation.TicketValidator; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.cas.web.CasAuthenticationFilter; |
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; |
||||
import org.springframework.security.core.userdetails.User; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
import static org.assertj.core.api.Assertions.fail; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.times; |
||||
import static org.mockito.Mockito.verify; |
||||
|
||||
/** |
||||
* Tests {@link CasAuthenticationProvider}. |
||||
* |
||||
* @author Ben Alex |
||||
* @author Scott Battaglia |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public class CasAuthenticationProviderTests { |
||||
|
||||
private UserDetails makeUserDetails() { |
||||
return new User("user", "password", true, true, true, true, |
||||
AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); |
||||
} |
||||
|
||||
private UserDetails makeUserDetailsFromAuthoritiesPopulator() { |
||||
return new User("user", "password", true, true, true, true, |
||||
AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B")); |
||||
} |
||||
|
||||
private ServiceProperties makeServiceProperties() { |
||||
final ServiceProperties serviceProperties = new ServiceProperties(); |
||||
serviceProperties.setSendRenew(false); |
||||
serviceProperties.setService("http://test.com"); |
||||
return serviceProperties; |
||||
} |
||||
|
||||
@Test |
||||
public void statefulAuthenticationIsSuccessful() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
StatelessTicketCache cache = new MockStatelessTicketCache(); |
||||
cap.setStatelessTicketCache(cache); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.afterPropertiesSet(); |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123"); |
||||
token.setDetails("details"); |
||||
Authentication result = cap.authenticate(token); |
||||
// Confirm ST-123 was NOT added to the cache
|
||||
assertThat(cache.getByTicketId("ST-456") == null).isTrue(); |
||||
if (!(result instanceof CasAuthenticationToken)) { |
||||
fail("Should have returned a CasAuthenticationToken"); |
||||
} |
||||
CasAuthenticationToken casResult = (CasAuthenticationToken) result; |
||||
assertThat(casResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); |
||||
assertThat(casResult.getCredentials()).isEqualTo("ST-123"); |
||||
assertThat(casResult.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_A")); |
||||
assertThat(casResult.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_B")); |
||||
assertThat(casResult.getKeyHash()).isEqualTo(cap.getKey().hashCode()); |
||||
assertThat(casResult.getDetails()).isEqualTo("details"); |
||||
// Now confirm the CasAuthenticationToken is automatically re-accepted.
|
||||
// To ensure TicketValidator not called again, set it to deliver an exception...
|
||||
cap.setTicketValidator(new MockTicketValidator(false)); |
||||
Authentication laterResult = cap.authenticate(result); |
||||
assertThat(laterResult).isEqualTo(result); |
||||
} |
||||
|
||||
@Test |
||||
public void statelessAuthenticationIsSuccessful() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
StatelessTicketCache cache = new MockStatelessTicketCache(); |
||||
cap.setStatelessTicketCache(cache); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456"); |
||||
token.setDetails("details"); |
||||
Authentication result = cap.authenticate(token); |
||||
// Confirm ST-456 was added to the cache
|
||||
assertThat(cache.getByTicketId("ST-456") != null).isTrue(); |
||||
if (!(result instanceof CasAuthenticationToken)) { |
||||
fail("Should have returned a CasAuthenticationToken"); |
||||
} |
||||
assertThat(result.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); |
||||
assertThat(result.getCredentials()).isEqualTo("ST-456"); |
||||
assertThat(result.getDetails()).isEqualTo("details"); |
||||
// Now try to authenticate again. To ensure TicketValidator not
|
||||
// called again, set it to deliver an exception...
|
||||
cap.setTicketValidator(new MockTicketValidator(false)); |
||||
// Previously created UsernamePasswordAuthenticationToken is OK
|
||||
Authentication newResult = cap.authenticate(token); |
||||
assertThat(newResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator()); |
||||
assertThat(newResult.getCredentials()).isEqualTo("ST-456"); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateAllNullService() throws Exception { |
||||
String serviceUrl = "https://service/context"; |
||||
ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class); |
||||
given(details.getServiceUrl()).willReturn(serviceUrl); |
||||
TicketValidator validator = mock(TicketValidator.class); |
||||
given(validator.validate(any(String.class), any(String.class))).willReturn(new AssertionImpl("rod")); |
||||
ServiceProperties serviceProperties = makeServiceProperties(); |
||||
serviceProperties.setAuthenticateAllArtifacts(true); |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setTicketValidator(validator); |
||||
cap.setServiceProperties(serviceProperties); |
||||
cap.afterPropertiesSet(); |
||||
String ticket = "ST-456"; |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); |
||||
Authentication result = cap.authenticate(token); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateAllAuthenticationIsSuccessful() throws Exception { |
||||
String serviceUrl = "https://service/context"; |
||||
ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class); |
||||
given(details.getServiceUrl()).willReturn(serviceUrl); |
||||
TicketValidator validator = mock(TicketValidator.class); |
||||
given(validator.validate(any(String.class), any(String.class))).willReturn(new AssertionImpl("rod")); |
||||
ServiceProperties serviceProperties = makeServiceProperties(); |
||||
serviceProperties.setAuthenticateAllArtifacts(true); |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setTicketValidator(validator); |
||||
cap.setServiceProperties(serviceProperties); |
||||
cap.afterPropertiesSet(); |
||||
String ticket = "ST-456"; |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket); |
||||
Authentication result = cap.authenticate(token); |
||||
verify(validator).validate(ticket, serviceProperties.getService()); |
||||
serviceProperties.setAuthenticateAllArtifacts(true); |
||||
result = cap.authenticate(token); |
||||
verify(validator, times(2)).validate(ticket, serviceProperties.getService()); |
||||
token.setDetails(details); |
||||
result = cap.authenticate(token); |
||||
verify(validator).validate(ticket, serviceUrl); |
||||
serviceProperties.setAuthenticateAllArtifacts(false); |
||||
serviceProperties.setService(null); |
||||
cap.setServiceProperties(serviceProperties); |
||||
cap.afterPropertiesSet(); |
||||
result = cap.authenticate(token); |
||||
verify(validator, times(2)).validate(ticket, serviceUrl); |
||||
token.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest())); |
||||
assertThatIllegalStateException().isThrownBy(() -> cap.authenticate(token)); |
||||
cap.setServiceProperties(null); |
||||
cap.afterPropertiesSet(); |
||||
assertThatIllegalStateException().isThrownBy(() -> cap.authenticate(token)); |
||||
} |
||||
|
||||
@Test |
||||
public void missingTicketIdIsDetected() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
StatelessTicketCache cache = new MockStatelessTicketCache(); |
||||
cap.setStatelessTicketCache(cache); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, ""); |
||||
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token)); |
||||
} |
||||
|
||||
@Test |
||||
public void invalidKeyIsDetected() throws Exception { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
StatelessTicketCache cache = new MockStatelessTicketCache(); |
||||
cap.setStatelessTicketCache(cache); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
CasAuthenticationToken token = new CasAuthenticationToken("WRONG_KEY", makeUserDetails(), "credentials", |
||||
AuthorityUtils.createAuthorityList("XX"), makeUserDetails(), assertion); |
||||
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token)); |
||||
} |
||||
|
||||
@Test |
||||
public void detectsMissingAuthoritiesPopulator() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setKey("qwerty"); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); |
||||
} |
||||
|
||||
@Test |
||||
public void detectsMissingKey() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); |
||||
} |
||||
|
||||
@Test |
||||
public void detectsMissingStatelessTicketCache() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
// set this explicitly to null to test failure
|
||||
cap.setStatelessTicketCache(null); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); |
||||
} |
||||
|
||||
@Test |
||||
public void detectsMissingTicketValidator() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> cap.afterPropertiesSet()); |
||||
} |
||||
|
||||
@Test |
||||
public void gettersAndSettersMatch() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
// TODO disabled because why do we need to expose this?
|
||||
// assertThat(cap.getUserDetailsService() != null).isTrue();
|
||||
assertThat(cap.getKey()).isEqualTo("qwerty"); |
||||
assertThat(cap.getStatelessTicketCache() != null).isTrue(); |
||||
assertThat(cap.getTicketValidator() != null).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void ignoresClassesItDoesNotSupport() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
TestingAuthenticationToken token = new TestingAuthenticationToken("user", "password", "ROLE_A"); |
||||
assertThat(cap.supports(TestingAuthenticationToken.class)).isFalse(); |
||||
// Try it anyway
|
||||
assertThat(cap.authenticate(token)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal() throws Exception { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator()); |
||||
cap.setKey("qwerty"); |
||||
cap.setStatelessTicketCache(new MockStatelessTicketCache()); |
||||
cap.setTicketValidator(new MockTicketValidator(true)); |
||||
cap.setServiceProperties(makeServiceProperties()); |
||||
cap.afterPropertiesSet(); |
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user", |
||||
"password", AuthorityUtils.createAuthorityList("ROLE_A")); |
||||
assertThat(cap.authenticate(token)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void supportsRequiredTokens() { |
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider(); |
||||
assertThat(cap.supports(UsernamePasswordAuthenticationToken.class)).isTrue(); |
||||
assertThat(cap.supports(CasAuthenticationToken.class)).isTrue(); |
||||
} |
||||
|
||||
private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService { |
||||
|
||||
@Override |
||||
public UserDetails loadUserDetails(final Authentication token) throws UsernameNotFoundException { |
||||
return makeUserDetailsFromAuthoritiesPopulator(); |
||||
} |
||||
|
||||
} |
||||
|
||||
private class MockStatelessTicketCache implements StatelessTicketCache { |
||||
|
||||
private Map<String, CasAuthenticationToken> cache = new HashMap<>(); |
||||
|
||||
@Override |
||||
public CasAuthenticationToken getByTicketId(String serviceTicket) { |
||||
return this.cache.get(serviceTicket); |
||||
} |
||||
|
||||
@Override |
||||
public void putTicketInCache(CasAuthenticationToken token) { |
||||
this.cache.put(token.getCredentials().toString(), token); |
||||
} |
||||
|
||||
@Override |
||||
public void removeTicketFromCache(CasAuthenticationToken token) { |
||||
throw new UnsupportedOperationException("mock method not implemented"); |
||||
} |
||||
|
||||
@Override |
||||
public void removeTicketFromCache(String serviceTicket) { |
||||
throw new UnsupportedOperationException("mock method not implemented"); |
||||
} |
||||
|
||||
} |
||||
|
||||
private class MockTicketValidator implements TicketValidator { |
||||
|
||||
private boolean returnTicket; |
||||
|
||||
MockTicketValidator(boolean returnTicket) { |
||||
this.returnTicket = returnTicket; |
||||
} |
||||
|
||||
@Override |
||||
public Assertion validate(final String ticket, final String service) { |
||||
if (this.returnTicket) { |
||||
return new AssertionImpl("rod"); |
||||
} |
||||
throw new BadCredentialsException("As requested from mock"); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,169 +0,0 @@
@@ -1,169 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.jasig.cas.client.validation.AssertionImpl; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.userdetails.User; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests {@link CasAuthenticationToken}. |
||||
* |
||||
* @author Ben Alex |
||||
*/ |
||||
public class CasAuthenticationTokenTests { |
||||
|
||||
private final List<GrantedAuthority> ROLES = AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"); |
||||
|
||||
private UserDetails makeUserDetails() { |
||||
return makeUserDetails("user"); |
||||
} |
||||
|
||||
private UserDetails makeUserDetails(final String name) { |
||||
return new User(name, "password", true, true, true, true, this.ROLES); |
||||
} |
||||
|
||||
@Test |
||||
public void testConstructorRejectsNulls() { |
||||
Assertion assertion = new AssertionImpl("test"); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken(null, makeUserDetails(), |
||||
"Password", this.ROLES, makeUserDetails(), assertion)); |
||||
assertThatIllegalArgumentException().isThrownBy( |
||||
() -> new CasAuthenticationToken("key", null, "Password", this.ROLES, makeUserDetails(), assertion)); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), null, |
||||
this.ROLES, makeUserDetails(), assertion)); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), |
||||
"Password", this.ROLES, makeUserDetails(), null)); |
||||
assertThatIllegalArgumentException().isThrownBy( |
||||
() -> new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, null, assertion)); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CasAuthenticationToken("key", makeUserDetails(), |
||||
"Password", AuthorityUtils.createAuthorityList("ROLE_1", null), makeUserDetails(), assertion)); |
||||
} |
||||
|
||||
@Test |
||||
public void constructorWhenEmptyKeyThenThrowsException() { |
||||
assertThatIllegalArgumentException().isThrownBy( |
||||
() -> new CasAuthenticationToken("", "user", "password", Collections.<GrantedAuthority>emptyList(), |
||||
new User("user", "password", Collections.<GrantedAuthority>emptyList()), null)); |
||||
} |
||||
|
||||
@Test |
||||
public void testEqualsWhenEqual() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
assertThat(token2).isEqualTo(token1); |
||||
} |
||||
|
||||
@Test |
||||
public void testGetters() { |
||||
// Build the proxy list returned in the ticket from CAS
|
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
assertThat(token.getKeyHash()).isEqualTo("key".hashCode()); |
||||
assertThat(token.getPrincipal()).isEqualTo(makeUserDetails()); |
||||
assertThat(token.getCredentials()).isEqualTo("Password"); |
||||
assertThat(token.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_ONE")); |
||||
assertThat(token.getAuthorities()).contains(new SimpleGrantedAuthority("ROLE_TWO")); |
||||
assertThat(token.getAssertion()).isEqualTo(assertion); |
||||
assertThat(token.getUserDetails().getUsername()).isEqualTo(makeUserDetails().getUsername()); |
||||
} |
||||
|
||||
@Test |
||||
public void testNoArgConstructorDoesntExist() { |
||||
assertThatExceptionOfType(NoSuchMethodException.class) |
||||
.isThrownBy(() -> CasAuthenticationToken.class.getDeclaredConstructor((Class[]) null)); |
||||
} |
||||
|
||||
@Test |
||||
public void testNotEqualsDueToAbstractParentEqualsCheck() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails("OTHER_NAME"), "Password", |
||||
this.ROLES, makeUserDetails(), assertion); |
||||
assertThat(!token1.equals(token2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testNotEqualsDueToDifferentAuthenticationClass() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", "Password", |
||||
this.ROLES); |
||||
assertThat(!token1.equals(token2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testNotEqualsDueToKey() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
CasAuthenticationToken token2 = new CasAuthenticationToken("DIFFERENT_KEY", makeUserDetails(), "Password", |
||||
this.ROLES, makeUserDetails(), assertion); |
||||
assertThat(!token1.equals(token2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testNotEqualsDueToAssertion() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
final Assertion assertion2 = new AssertionImpl("test"); |
||||
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
CasAuthenticationToken token2 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion2); |
||||
assertThat(!token1.equals(token2)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testSetAuthenticated() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
assertThat(token.isAuthenticated()).isTrue(); |
||||
token.setAuthenticated(false); |
||||
assertThat(!token.isAuthenticated()).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testToString() { |
||||
final Assertion assertion = new AssertionImpl("test"); |
||||
CasAuthenticationToken token = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES, |
||||
makeUserDetails(), assertion); |
||||
String result = token.toString(); |
||||
assertThat(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1).isTrue(); |
||||
} |
||||
|
||||
} |
||||
@ -1,46 +0,0 @@
@@ -1,46 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Test cases for the @link {@link NullStatelessTicketCache} |
||||
* |
||||
* @author Scott Battaglia |
||||
* |
||||
*/ |
||||
public class NullStatelessTicketCacheTests extends AbstractStatelessTicketCacheTests { |
||||
|
||||
private StatelessTicketCache cache = new NullStatelessTicketCache(); |
||||
|
||||
@Test |
||||
public void testGetter() { |
||||
assertThat(this.cache.getByTicketId(null)).isNull(); |
||||
assertThat(this.cache.getByTicketId("test")).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void testInsertAndGet() { |
||||
final CasAuthenticationToken token = getToken(); |
||||
this.cache.putTicketInCache(token); |
||||
assertThat(this.cache.getByTicketId((String) token.getCredentials())).isNull(); |
||||
} |
||||
|
||||
} |
||||
@ -1,65 +0,0 @@
@@ -1,65 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.authentication; |
||||
|
||||
import org.junit.jupiter.api.BeforeAll; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.cache.CacheManager; |
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests |
||||
* {@link org.springframework.security.cas.authentication.SpringCacheBasedTicketCache}. |
||||
* |
||||
* @author Marten Deinum |
||||
* @since 3.2 |
||||
*/ |
||||
public class SpringCacheBasedTicketCacheTests extends AbstractStatelessTicketCacheTests { |
||||
|
||||
private static CacheManager cacheManager; |
||||
|
||||
@BeforeAll |
||||
public static void initCacheManaer() { |
||||
cacheManager = new ConcurrentMapCacheManager(); |
||||
cacheManager.getCache("castickets"); |
||||
} |
||||
|
||||
@Test |
||||
public void testCacheOperation() throws Exception { |
||||
SpringCacheBasedTicketCache cache = new SpringCacheBasedTicketCache(cacheManager.getCache("castickets")); |
||||
final CasAuthenticationToken token = getToken(); |
||||
// Check it gets stored in the cache
|
||||
cache.putTicketInCache(token); |
||||
assertThat(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")).isEqualTo(token); |
||||
// Check it gets removed from the cache
|
||||
cache.removeTicketFromCache(getToken()); |
||||
assertThat(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")).isNull(); |
||||
// Check it doesn't return values for null or unknown service tickets
|
||||
assertThat(cache.getByTicketId(null)).isNull(); |
||||
assertThat(cache.getByTicketId("UNKNOWN_SERVICE_TICKET")).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void testStartupDetectsMissingCache() throws Exception { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> new SpringCacheBasedTicketCache(null)); |
||||
} |
||||
|
||||
} |
||||
@ -1,153 +0,0 @@
@@ -1,153 +0,0 @@
|
||||
/* |
||||
* Copyright 2015-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.jackson2; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Date; |
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.jasig.cas.client.authentication.AttributePrincipalImpl; |
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.jasig.cas.client.validation.AssertionImpl; |
||||
import org.json.JSONException; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.skyscreamer.jsonassert.JSONAssert; |
||||
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationToken; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.userdetails.User; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.jackson2.SecurityJackson2Modules; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Jitendra Singh |
||||
* @since 4.2 |
||||
*/ |
||||
public class CasAuthenticationTokenMixinTests { |
||||
|
||||
private static final String KEY = "casKey"; |
||||
|
||||
private static final String PASSWORD = "\"1234\""; |
||||
|
||||
private static final Date START_DATE = new Date(); |
||||
|
||||
private static final Date END_DATE = new Date(); |
||||
|
||||
public static final String AUTHORITY_JSON = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"authority\": \"ROLE_USER\"}"; |
||||
|
||||
public static final String AUTHORITIES_SET_JSON = "[\"java.util.Collections$UnmodifiableSet\", [" + AUTHORITY_JSON |
||||
+ "]]"; |
||||
|
||||
public static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", [" |
||||
+ AUTHORITY_JSON + "]]"; |
||||
|
||||
// @formatter:off
|
||||
public static final String USER_JSON = "{" |
||||
+ "\"@class\": \"org.springframework.security.core.userdetails.User\", " |
||||
+ "\"username\": \"admin\"," |
||||
+ " \"password\": " + PASSWORD + ", " |
||||
+ "\"accountNonExpired\": true, " |
||||
+ "\"accountNonLocked\": true, " |
||||
+ "\"credentialsNonExpired\": true, " |
||||
+ "\"enabled\": true, " |
||||
+ "\"authorities\": " + AUTHORITIES_SET_JSON |
||||
+ "}"; |
||||
// @formatter:on
|
||||
private static final String CAS_TOKEN_JSON = "{" |
||||
+ "\"@class\": \"org.springframework.security.cas.authentication.CasAuthenticationToken\", " |
||||
+ "\"keyHash\": " + KEY.hashCode() + "," + "\"principal\": " + USER_JSON + ", " + "\"credentials\": " |
||||
+ PASSWORD + ", " + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + "\"userDetails\": " + USER_JSON |
||||
+ "," + "\"authenticated\": true, " + "\"details\": null," + "\"assertion\": {" |
||||
+ "\"@class\": \"org.jasig.cas.client.validation.AssertionImpl\", " + "\"principal\": {" |
||||
+ "\"@class\": \"org.jasig.cas.client.authentication.AttributePrincipalImpl\", " |
||||
+ "\"name\": \"assertName\", " + "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}, " |
||||
+ "\"proxyGrantingTicket\": null, " + "\"proxyRetriever\": null" + "}, " |
||||
+ "\"validFromDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " |
||||
+ "\"validUntilDate\": [\"java.util.Date\", " + END_DATE.getTime() + "]," |
||||
+ "\"authenticationDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " |
||||
+ "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}" + "}" + "}"; |
||||
|
||||
private static final String CAS_TOKEN_CLEARED_JSON = CAS_TOKEN_JSON.replaceFirst(PASSWORD, "null"); |
||||
|
||||
protected ObjectMapper mapper; |
||||
|
||||
@BeforeEach |
||||
public void setup() { |
||||
this.mapper = new ObjectMapper(); |
||||
ClassLoader loader = getClass().getClassLoader(); |
||||
this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); |
||||
} |
||||
|
||||
@Test |
||||
public void serializeCasAuthenticationTest() throws JsonProcessingException, JSONException { |
||||
CasAuthenticationToken token = createCasAuthenticationToken(); |
||||
String actualJson = this.mapper.writeValueAsString(token); |
||||
JSONAssert.assertEquals(CAS_TOKEN_JSON, actualJson, true); |
||||
} |
||||
|
||||
@Test |
||||
public void serializeCasAuthenticationTestAfterEraseCredentialInvoked() |
||||
throws JsonProcessingException, JSONException { |
||||
CasAuthenticationToken token = createCasAuthenticationToken(); |
||||
token.eraseCredentials(); |
||||
String actualJson = this.mapper.writeValueAsString(token); |
||||
JSONAssert.assertEquals(CAS_TOKEN_CLEARED_JSON, actualJson, true); |
||||
} |
||||
|
||||
@Test |
||||
public void deserializeCasAuthenticationTestAfterEraseCredentialInvoked() throws Exception { |
||||
CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_CLEARED_JSON, CasAuthenticationToken.class); |
||||
assertThat(((UserDetails) token.getPrincipal()).getPassword()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void deserializeCasAuthenticationTest() throws IOException { |
||||
CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_JSON, CasAuthenticationToken.class); |
||||
assertThat(token).isNotNull(); |
||||
assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); |
||||
assertThat(((User) token.getPrincipal()).getUsername()).isEqualTo("admin"); |
||||
assertThat(((User) token.getPrincipal()).getPassword()).isEqualTo("1234"); |
||||
assertThat(token.getUserDetails()).isNotNull().isInstanceOf(User.class); |
||||
assertThat(token.getAssertion()).isNotNull().isInstanceOf(AssertionImpl.class); |
||||
assertThat(token.getKeyHash()).isEqualTo(KEY.hashCode()); |
||||
assertThat(token.getUserDetails().getAuthorities()).extracting(GrantedAuthority::getAuthority) |
||||
.containsOnly("ROLE_USER"); |
||||
assertThat(token.getAssertion().getAuthenticationDate()).isEqualTo(START_DATE); |
||||
assertThat(token.getAssertion().getValidFromDate()).isEqualTo(START_DATE); |
||||
assertThat(token.getAssertion().getValidUntilDate()).isEqualTo(END_DATE); |
||||
assertThat(token.getAssertion().getPrincipal().getName()).isEqualTo("assertName"); |
||||
assertThat(token.getAssertion().getAttributes()).hasSize(0); |
||||
} |
||||
|
||||
private CasAuthenticationToken createCasAuthenticationToken() { |
||||
User principal = new User("admin", "1234", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))); |
||||
Collection<? extends GrantedAuthority> authorities = Collections |
||||
.singletonList(new SimpleGrantedAuthority("ROLE_USER")); |
||||
Assertion assertion = new AssertionImpl(new AttributePrincipalImpl("assertName"), START_DATE, END_DATE, |
||||
START_DATE, Collections.<String, Object>emptyMap()); |
||||
return new CasAuthenticationToken(KEY, principal, principal.getPassword(), authorities, |
||||
new User("admin", "1234", authorities), assertion); |
||||
} |
||||
|
||||
} |
||||
@ -1,63 +0,0 @@
@@ -1,63 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2017 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.userdetails; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import org.jasig.cas.client.authentication.AttributePrincipal; |
||||
import org.jasig.cas.client.validation.Assertion; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* @author Luke Taylor |
||||
*/ |
||||
public class GrantedAuthorityFromAssertionAttributesUserDetailsServiceTests { |
||||
|
||||
@Test |
||||
public void correctlyExtractsNamedAttributesFromAssertionAndConvertsThemToAuthorities() { |
||||
GrantedAuthorityFromAssertionAttributesUserDetailsService uds = new GrantedAuthorityFromAssertionAttributesUserDetailsService( |
||||
new String[] { "a", "b", "c", "d" }); |
||||
uds.setConvertToUpperCase(false); |
||||
Assertion assertion = mock(Assertion.class); |
||||
AttributePrincipal principal = mock(AttributePrincipal.class); |
||||
Map<String, Object> attributes = new HashMap<>(); |
||||
attributes.put("a", Arrays.asList("role_a1", "role_a2")); |
||||
attributes.put("b", "role_b"); |
||||
attributes.put("c", "role_c"); |
||||
attributes.put("d", null); |
||||
attributes.put("someother", "unused"); |
||||
given(assertion.getPrincipal()).willReturn(principal); |
||||
given(principal.getAttributes()).willReturn(attributes); |
||||
given(principal.getName()).willReturn("somebody"); |
||||
CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, "ticket"); |
||||
UserDetails user = uds.loadUserDetails(token); |
||||
Set<String> roles = AuthorityUtils.authorityListToSet(user.getAuthorities()); |
||||
assertThat(roles).containsExactlyInAnyOrder("role_a1", "role_a2", "role_b", "role_c"); |
||||
} |
||||
|
||||
} |
||||
@ -1,98 +0,0 @@
@@ -1,98 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web; |
||||
|
||||
import java.net.URLEncoder; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests {@link CasAuthenticationEntryPoint}. |
||||
* |
||||
* @author Ben Alex |
||||
*/ |
||||
public class CasAuthenticationEntryPointTests { |
||||
|
||||
@Test |
||||
public void testDetectsMissingLoginFormUrl() throws Exception { |
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); |
||||
ep.setServiceProperties(new ServiceProperties()); |
||||
assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet) |
||||
.withMessage("loginUrl must be specified"); |
||||
} |
||||
|
||||
@Test |
||||
public void testDetectsMissingServiceProperties() throws Exception { |
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); |
||||
ep.setLoginUrl("https://cas/login"); |
||||
assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet) |
||||
.withMessage("serviceProperties must be specified"); |
||||
} |
||||
|
||||
@Test |
||||
public void testGettersSetters() { |
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); |
||||
ep.setLoginUrl("https://cas/login"); |
||||
assertThat(ep.getLoginUrl()).isEqualTo("https://cas/login"); |
||||
ep.setServiceProperties(new ServiceProperties()); |
||||
assertThat(ep.getServiceProperties() != null).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testNormalOperationWithRenewFalse() throws Exception { |
||||
ServiceProperties sp = new ServiceProperties(); |
||||
sp.setSendRenew(false); |
||||
sp.setService("https://mycompany.com/bigWebApp/login/cas"); |
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); |
||||
ep.setLoginUrl("https://cas/login"); |
||||
ep.setServiceProperties(sp); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
request.setRequestURI("/some_path"); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
ep.afterPropertiesSet(); |
||||
ep.commence(request, response, null); |
||||
assertThat( |
||||
"https://cas/login?service=" + URLEncoder.encode("https://mycompany.com/bigWebApp/login/cas", "UTF-8")) |
||||
.isEqualTo(response.getRedirectedUrl()); |
||||
} |
||||
|
||||
@Test |
||||
public void testNormalOperationWithRenewTrue() throws Exception { |
||||
ServiceProperties sp = new ServiceProperties(); |
||||
sp.setSendRenew(true); |
||||
sp.setService("https://mycompany.com/bigWebApp/login/cas"); |
||||
CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint(); |
||||
ep.setLoginUrl("https://cas/login"); |
||||
ep.setServiceProperties(sp); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
request.setRequestURI("/some_path"); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
ep.afterPropertiesSet(); |
||||
ep.commence(request, response, null); |
||||
assertThat("https://cas/login?service=" |
||||
+ URLEncoder.encode("https://mycompany.com/bigWebApp/login/cas", "UTF-8") + "&renew=true") |
||||
.isEqualTo(response.getRedirectedUrl()); |
||||
} |
||||
|
||||
} |
||||
@ -1,199 +0,0 @@
@@ -1,199 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web; |
||||
|
||||
import jakarta.servlet.FilterChain; |
||||
|
||||
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; |
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
import static org.mockito.Mockito.verifyZeroInteractions; |
||||
|
||||
/** |
||||
* Tests {@link CasAuthenticationFilter}. |
||||
* |
||||
* @author Ben Alex |
||||
* @author Rob Winch |
||||
*/ |
||||
public class CasAuthenticationFilterTests { |
||||
|
||||
@AfterEach |
||||
public void tearDown() { |
||||
SecurityContextHolder.clearContext(); |
||||
} |
||||
|
||||
@Test |
||||
public void testGettersSetters() { |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); |
||||
filter.setProxyReceptorUrl("/someurl"); |
||||
filter.setServiceProperties(new ServiceProperties()); |
||||
} |
||||
|
||||
@Test |
||||
public void testNormalOperation() throws Exception { |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
request.setServletPath("/login/cas"); |
||||
request.addParameter("ticket", "ST-0-ER94xMJmn6pha35CQRoZ"); |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setAuthenticationManager((a) -> a); |
||||
assertThat(filter.requiresAuthentication(request, new MockHttpServletResponse())).isTrue(); |
||||
Authentication result = filter.attemptAuthentication(request, new MockHttpServletResponse()); |
||||
assertThat(result != null).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testNullServiceTicketHandledGracefully() throws Exception { |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setAuthenticationManager((a) -> { |
||||
throw new BadCredentialsException("Rejected"); |
||||
}); |
||||
assertThatExceptionOfType(AuthenticationException.class).isThrownBy( |
||||
() -> filter.attemptAuthentication(new MockHttpServletRequest(), new MockHttpServletResponse())); |
||||
} |
||||
|
||||
@Test |
||||
public void testRequiresAuthenticationFilterProcessUrl() { |
||||
String url = "/login/cas"; |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setFilterProcessesUrl(url); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
request.setServletPath(url); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void testRequiresAuthenticationProxyRequest() { |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
request.setServletPath("/pgtCallback"); |
||||
assertThat(filter.requiresAuthentication(request, response)).isFalse(); |
||||
filter.setProxyReceptorUrl(request.getServletPath()); |
||||
assertThat(filter.requiresAuthentication(request, response)).isFalse(); |
||||
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
request.setServletPath("/other"); |
||||
assertThat(filter.requiresAuthentication(request, response)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void testRequiresAuthenticationAuthAll() { |
||||
ServiceProperties properties = new ServiceProperties(); |
||||
properties.setAuthenticateAllArtifacts(true); |
||||
String url = "/login/cas"; |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setFilterProcessesUrl(url); |
||||
filter.setServiceProperties(properties); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
request.setServletPath(url); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
request.setServletPath("/other"); |
||||
assertThat(filter.requiresAuthentication(request, response)).isFalse(); |
||||
request.setParameter(properties.getArtifactParameter(), "value"); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
SecurityContextHolder.getContext().setAuthentication(new AnonymousAuthenticationToken("key", "principal", |
||||
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"))); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal")); |
||||
assertThat(filter.requiresAuthentication(request, response)).isTrue(); |
||||
SecurityContextHolder.getContext() |
||||
.setAuthentication(new TestingAuthenticationToken("un", "principal", "ROLE_ANONYMOUS")); |
||||
assertThat(filter.requiresAuthentication(request, response)).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void testAuthenticateProxyUrl() throws Exception { |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
request.setServletPath("/pgtCallback"); |
||||
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); |
||||
filter.setProxyReceptorUrl(request.getServletPath()); |
||||
assertThat(filter.attemptAuthentication(request, response)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void testDoFilterAuthenticateAll() throws Exception { |
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class); |
||||
AuthenticationManager manager = mock(AuthenticationManager.class); |
||||
Authentication authentication = new TestingAuthenticationToken("un", "pwd", "ROLE_USER"); |
||||
given(manager.authenticate(any(Authentication.class))).willReturn(authentication); |
||||
ServiceProperties serviceProperties = new ServiceProperties(); |
||||
serviceProperties.setAuthenticateAllArtifacts(true); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
request.setParameter("ticket", "ST-1-123"); |
||||
request.setServletPath("/authenticate"); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain chain = mock(FilterChain.class); |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
filter.setServiceProperties(serviceProperties); |
||||
filter.setAuthenticationSuccessHandler(successHandler); |
||||
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); |
||||
filter.setAuthenticationManager(manager); |
||||
filter.afterPropertiesSet(); |
||||
filter.doFilter(request, response, chain); |
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull() |
||||
.withFailMessage("Authentication should not be null"); |
||||
verify(chain).doFilter(request, response); |
||||
verifyZeroInteractions(successHandler); |
||||
// validate for when the filterProcessUrl matches
|
||||
filter.setFilterProcessesUrl(request.getServletPath()); |
||||
SecurityContextHolder.clearContext(); |
||||
filter.doFilter(request, response, chain); |
||||
verifyNoMoreInteractions(chain); |
||||
verify(successHandler).onAuthenticationSuccess(request, response, authentication); |
||||
} |
||||
|
||||
// SEC-1592
|
||||
@Test |
||||
public void testChainNotInvokedForProxyReceptor() throws Exception { |
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter(); |
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain chain = mock(FilterChain.class); |
||||
request.setServletPath("/pgtCallback"); |
||||
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class)); |
||||
filter.setProxyReceptorUrl(request.getServletPath()); |
||||
filter.doFilter(request, response, chain); |
||||
verifyZeroInteractions(chain); |
||||
} |
||||
|
||||
} |
||||
@ -1,67 +0,0 @@
@@ -1,67 +0,0 @@
|
||||
/* |
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.security.cas.SamlServiceProperties; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests {@link ServiceProperties}. |
||||
* |
||||
* @author Ben Alex |
||||
*/ |
||||
public class ServicePropertiesTests { |
||||
|
||||
@Test |
||||
public void detectsMissingService() throws Exception { |
||||
ServiceProperties sp = new ServiceProperties(); |
||||
assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); |
||||
} |
||||
|
||||
@Test |
||||
public void nullServiceWhenAuthenticateAllTokens() throws Exception { |
||||
ServiceProperties sp = new ServiceProperties(); |
||||
sp.setAuthenticateAllArtifacts(true); |
||||
assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); |
||||
sp.setAuthenticateAllArtifacts(false); |
||||
assertThatIllegalArgumentException().isThrownBy(sp::afterPropertiesSet); |
||||
} |
||||
|
||||
@Test |
||||
public void testGettersSetters() throws Exception { |
||||
ServiceProperties[] sps = { new ServiceProperties(), new SamlServiceProperties() }; |
||||
for (ServiceProperties sp : sps) { |
||||
sp.setSendRenew(false); |
||||
assertThat(sp.isSendRenew()).isFalse(); |
||||
sp.setSendRenew(true); |
||||
assertThat(sp.isSendRenew()).isTrue(); |
||||
sp.setArtifactParameter("notticket"); |
||||
assertThat(sp.getArtifactParameter()).isEqualTo("notticket"); |
||||
sp.setServiceParameter("notservice"); |
||||
assertThat(sp.getServiceParameter()).isEqualTo("notservice"); |
||||
sp.setService("https://mycompany.com/service"); |
||||
assertThat(sp.getService()).isEqualTo("https://mycompany.com/service"); |
||||
sp.afterPropertiesSet(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,132 +0,0 @@
@@ -1,132 +0,0 @@
|
||||
/* |
||||
* Copyright 2011-2016 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.cas.web.authentication; |
||||
|
||||
import java.util.regex.Pattern; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext; |
||||
import org.springframework.context.support.GenericXmlApplicationContext; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.security.cas.ServiceProperties; |
||||
import org.springframework.security.web.util.UrlUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Rob Winch |
||||
*/ |
||||
public class DefaultServiceAuthenticationDetailsTests { |
||||
|
||||
private DefaultServiceAuthenticationDetails details; |
||||
|
||||
private MockHttpServletRequest request; |
||||
|
||||
private Pattern artifactPattern; |
||||
|
||||
private String casServiceUrl; |
||||
|
||||
private ConfigurableApplicationContext context; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.casServiceUrl = "https://localhost:8443/j_spring_security_cas"; |
||||
this.request = new MockHttpServletRequest(); |
||||
this.request.setScheme("https"); |
||||
this.request.setServerName("localhost"); |
||||
this.request.setServerPort(8443); |
||||
this.request.setRequestURI("/cas-sample/secure/"); |
||||
this.artifactPattern = DefaultServiceAuthenticationDetails |
||||
.createArtifactPattern(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void cleanup() { |
||||
if (this.context != null) { |
||||
this.context.close(); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlNullQuery() throws Exception { |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
assertThat(this.details.getServiceUrl()).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlTicketOnlyParam() throws Exception { |
||||
this.request.setQueryString("ticket=123"); |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
String serviceUrl = this.details.getServiceUrl(); |
||||
this.request.setQueryString(null); |
||||
assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlTicketFirstMultiParam() throws Exception { |
||||
this.request.setQueryString("ticket=123&other=value"); |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
String serviceUrl = this.details.getServiceUrl(); |
||||
this.request.setQueryString("other=value"); |
||||
assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlTicketLastMultiParam() throws Exception { |
||||
this.request.setQueryString("other=value&ticket=123"); |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
String serviceUrl = this.details.getServiceUrl(); |
||||
this.request.setQueryString("other=value"); |
||||
assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlTicketMiddleMultiParam() throws Exception { |
||||
this.request.setQueryString("other=value&ticket=123&last=this"); |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
String serviceUrl = this.details.getServiceUrl(); |
||||
this.request.setQueryString("other=value&last=this"); |
||||
assertThat(serviceUrl).isEqualTo(UrlUtils.buildFullRequestUrl(this.request)); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlDoesNotUseHostHeader() throws Exception { |
||||
this.casServiceUrl = "https://example.com/j_spring_security_cas"; |
||||
this.request.setServerName("evil.com"); |
||||
this.details = new DefaultServiceAuthenticationDetails(this.casServiceUrl, this.request, this.artifactPattern); |
||||
assertThat(this.details.getServiceUrl()).isEqualTo("https://example.com/cas-sample/secure/"); |
||||
} |
||||
|
||||
@Test |
||||
public void getServiceUrlDoesNotUseHostHeaderExplicit() { |
||||
this.casServiceUrl = "https://example.com/j_spring_security_cas"; |
||||
this.request.setServerName("evil.com"); |
||||
ServiceAuthenticationDetails details = loadServiceAuthenticationDetails( |
||||
"defaultserviceauthenticationdetails-explicit.xml"); |
||||
assertThat(details.getServiceUrl()).isEqualTo("https://example.com/cas-sample/secure/"); |
||||
} |
||||
|
||||
private ServiceAuthenticationDetails loadServiceAuthenticationDetails(String resourceName) { |
||||
this.context = new GenericXmlApplicationContext(getClass(), resourceName); |
||||
ServiceAuthenticationDetailsSource source = this.context.getBean(ServiceAuthenticationDetailsSource.class); |
||||
return source.buildDetails(this.request); |
||||
} |
||||
|
||||
} |
||||
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
<configuration> |
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> |
||||
<encoder> |
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> |
||||
</encoder> |
||||
</appender> |
||||
|
||||
<logger name="org.springframework.security" level="${sec.log.level:-WARN}"/> |
||||
|
||||
|
||||
<root level="${root.level:-WARN}"> |
||||
<appender-ref ref="STDOUT" /> |
||||
</root> |
||||
|
||||
</configuration> |
||||
@ -1,23 +0,0 @@
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<beans xmlns="http://www.springframework.org/schema/beans" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> |
||||
|
||||
<bean id="serviceProperties" |
||||
class="org.springframework.security.cas.ServiceProperties"> |
||||
<property name="service" |
||||
value="https://example.com/j_spring_security_cas"/> |
||||
<property name="sendRenew" value="false"/> |
||||
</bean> |
||||
<bean id="serviceProperties2" |
||||
class="org.springframework.security.cas.ServiceProperties"> |
||||
<property name="service" |
||||
value="https://example2.com/j_spring_security_cas"/> |
||||
<property name="sendRenew" value="false"/> |
||||
</bean> |
||||
|
||||
<bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"> |
||||
<constructor-arg ref="serviceProperties"/> |
||||
</bean> |
||||
</beans> |
||||
@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<beans xmlns="http://www.springframework.org/schema/beans" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> |
||||
|
||||
<bean id="serviceProperties" |
||||
class="org.springframework.security.cas.ServiceProperties"> |
||||
<property name="service" |
||||
value="https://example.com/j_spring_security_cas"/> |
||||
<property name="sendRenew" value="false"/> |
||||
</bean> |
||||
|
||||
<bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/> |
||||
</beans> |
||||
@ -1,463 +0,0 @@
@@ -1,463 +0,0 @@
|
||||
[[servlet-cas]] |
||||
= CAS Authentication |
||||
|
||||
[[cas-overview]] |
||||
== Overview |
||||
JA-SIG produces an enterprise-wide single sign on system known as CAS. |
||||
Unlike other initiatives, JA-SIG's Central Authentication Service is open source, widely used, simple to understand, platform independent, and supports proxy capabilities. |
||||
Spring Security fully supports CAS, and provides an easy migration path from single-application deployments of Spring Security through to multiple-application deployments secured by an enterprise-wide CAS server. |
||||
|
||||
You can learn more about CAS at https://www.apereo.org. |
||||
You will also need to visit this site to download the CAS Server files. |
||||
|
||||
[[cas-how-it-works]] |
||||
== How CAS Works |
||||
Whilst the CAS web site contains documents that detail the architecture of CAS, we present the general overview again here within the context of Spring Security. |
||||
Spring Security 3.x supports CAS 3. |
||||
At the time of writing, the CAS server was at version 3.4. |
||||
|
||||
Somewhere in your enterprise you will need to setup a CAS server. |
||||
The CAS server is simply a standard WAR file, so there isn't anything difficult about setting up your server. |
||||
Inside the WAR file you will customise the login and other single sign on pages displayed to users. |
||||
|
||||
When deploying a CAS 3.4 server, you will also need to specify an `AuthenticationHandler` in the `deployerConfigContext.xml` included with CAS. |
||||
The `AuthenticationHandler` has a simple method that returns a boolean as to whether a given set of Credentials is valid. |
||||
Your `AuthenticationHandler` implementation will need to link into some type of backend authentication repository, such as an LDAP server or database. |
||||
CAS itself includes numerous ``AuthenticationHandler``s out of the box to assist with this. |
||||
When you download and deploy the server war file, it is set up to successfully authenticate users who enter a password matching their username, which is useful for testing. |
||||
|
||||
Apart from the CAS server itself, the other key players are of course the secure web applications deployed throughout your enterprise. |
||||
These web applications are known as "services". |
||||
There are three types of services. |
||||
Those that authenticate service tickets, those that can obtain proxy tickets, and those that authenticate proxy tickets. |
||||
Authenticating a proxy ticket differs because the list of proxies must be validated and often times a proxy ticket can be reused. |
||||
|
||||
|
||||
[[cas-sequence]] |
||||
=== Spring Security and CAS Interaction Sequence |
||||
The basic interaction between a web browser, CAS server and a Spring Security-secured service is as follows: |
||||
|
||||
* The web user is browsing the service's public pages. |
||||
CAS or Spring Security is not involved. |
||||
* The user eventually requests a page that is either secure or one of the beans it uses is secure. |
||||
Spring Security's `ExceptionTranslationFilter` will detect the `AccessDeniedException` or `AuthenticationException`. |
||||
* Because the user's `Authentication` object (or lack thereof) caused an `AuthenticationException`, the `ExceptionTranslationFilter` will call the configured `AuthenticationEntryPoint`. |
||||
If using CAS, this will be the `CasAuthenticationEntryPoint` class. |
||||
* The `CasAuthenticationEntryPoint` will redirect the user's browser to the CAS server. |
||||
It will also indicate a `service` parameter, which is the callback URL for the Spring Security service (your application). |
||||
For example, the URL to which the browser is redirected might be https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas. |
||||
* After the user's browser redirects to CAS, they will be prompted for their username and password. |
||||
If the user presents a session cookie which indicates they've previously logged on, they will not be prompted to login again (there is an exception to this procedure, which we'll cover later). |
||||
CAS will use the `PasswordHandler` (or `AuthenticationHandler` if using CAS 3.0) discussed above to decide whether the username and password is valid. |
||||
* Upon successful login, CAS will redirect the user's browser back to the original service. |
||||
It will also include a `ticket` parameter, which is an opaque string representing the "service ticket". |
||||
Continuing our earlier example, the URL the browser is redirected to might be https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ. |
||||
* Back in the service web application, the `CasAuthenticationFilter` is always listening for requests to `/login/cas` (this is configurable, but we'll use the defaults in this introduction). |
||||
The processing filter will construct a `UsernamePasswordAuthenticationToken` representing the service ticket. |
||||
The principal will be equal to `CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`, whilst the credentials will be the service ticket opaque value. |
||||
This authentication request will then be handed to the configured `AuthenticationManager`. |
||||
* The `AuthenticationManager` implementation will be the `ProviderManager`, which is in turn configured with the `CasAuthenticationProvider`. |
||||
The `CasAuthenticationProvider` only responds to ``UsernamePasswordAuthenticationToken``s containing the CAS-specific principal (such as `CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`) and ``CasAuthenticationToken``s (discussed later). |
||||
* `CasAuthenticationProvider` will validate the service ticket using a `TicketValidator` implementation. |
||||
This will typically be a `Cas20ServiceTicketValidator` which is one of the classes included in the CAS client library. |
||||
In the event the application needs to validate proxy tickets, the `Cas20ProxyTicketValidator` is used. |
||||
The `TicketValidator` makes an HTTPS request to the CAS server in order to validate the service ticket. |
||||
It may also include a proxy callback URL, which is included in this example: https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor. |
||||
* Back on the CAS server, the validation request will be received. |
||||
If the presented service ticket matches the service URL the ticket was issued to, CAS will provide an affirmative response in XML indicating the username. |
||||
If any proxy was involved in the authentication (discussed below), the list of proxies is also included in the XML response. |
||||
* [OPTIONAL] If the request to the CAS validation service included the proxy callback URL (in the `pgtUrl` parameter), CAS will include a `pgtIou` string in the XML response. |
||||
This `pgtIou` represents a proxy-granting ticket IOU. |
||||
The CAS server will then create its own HTTPS connection back to the `pgtUrl`. |
||||
This is to mutually authenticate the CAS server and the claimed service URL. |
||||
The HTTPS connection will be used to send a proxy granting ticket to the original web application. |
||||
For example, https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH. |
||||
* The `Cas20TicketValidator` will parse the XML received from the CAS server. |
||||
It will return to the `CasAuthenticationProvider` a `TicketResponse`, which includes the username (mandatory), proxy list (if any were involved), and proxy-granting ticket IOU (if the proxy callback was requested). |
||||
* Next `CasAuthenticationProvider` will call a configured `CasProxyDecider`. |
||||
The `CasProxyDecider` indicates whether the proxy list in the `TicketResponse` is acceptable to the service. |
||||
Several implementations are provided with Spring Security: `RejectProxyTickets`, `AcceptAnyCasProxy` and `NamedCasProxyDecider`. |
||||
These names are largely self-explanatory, except `NamedCasProxyDecider` which allows a `List` of trusted proxies to be provided. |
||||
* `CasAuthenticationProvider` will next request a `AuthenticationUserDetailsService` to load the `GrantedAuthority` objects that apply to the user contained in the `Assertion`. |
||||
* If there were no problems, `CasAuthenticationProvider` constructs a `CasAuthenticationToken` including the details contained in the `TicketResponse` and the ``GrantedAuthority``s. |
||||
* Control then returns to `CasAuthenticationFilter`, which places the created `CasAuthenticationToken` in the security context. |
||||
* The user's browser is redirected to the original page that caused the `AuthenticationException` (or a custom destination depending on the configuration). |
||||
|
||||
It's good that you're still here! |
||||
Let's now look at how this is configured |
||||
|
||||
[[cas-client]] |
||||
== Configuration of CAS Client |
||||
The web application side of CAS is made easy due to Spring Security. |
||||
It is assumed you already know the basics of using Spring Security, so these are not covered again below. |
||||
We'll assume a namespace based configuration is being used and add in the CAS beans as required. |
||||
Each section builds upon the previous section. |
||||
A full CAS sample application can be found in the Spring Security xref:samples.adoc#samples[Samples]. |
||||
|
||||
|
||||
[[cas-st]] |
||||
=== Service Ticket Authentication |
||||
This section describes how to setup Spring Security to authenticate Service Tickets. |
||||
Often times this is all a web application requires. |
||||
You will need to add a `ServiceProperties` bean to your application context. |
||||
This represents your CAS service: |
||||
|
||||
[source,xml] |
||||
---- |
||||
<bean id="serviceProperties" |
||||
class="org.springframework.security.cas.ServiceProperties"> |
||||
<property name="service" |
||||
value="https://localhost:8443/cas-sample/login/cas"/> |
||||
<property name="sendRenew" value="false"/> |
||||
</bean> |
||||
---- |
||||
|
||||
The `service` must equal a URL that will be monitored by the `CasAuthenticationFilter`. |
||||
The `sendRenew` defaults to false, but should be set to true if your application is particularly sensitive. |
||||
What this parameter does is tell the CAS login service that a single sign on login is unacceptable. |
||||
Instead, the user will need to re-enter their username and password in order to gain access to the service. |
||||
|
||||
The following beans should be configured to commence the CAS authentication process (assuming you're using a namespace configuration): |
||||
|
||||
[source,xml] |
||||
---- |
||||
<security:http entry-point-ref="casEntryPoint"> |
||||
... |
||||
<security:custom-filter position="CAS_FILTER" ref="casFilter" /> |
||||
</security:http> |
||||
|
||||
<bean id="casFilter" |
||||
class="org.springframework.security.cas.web.CasAuthenticationFilter"> |
||||
<property name="authenticationManager" ref="authenticationManager"/> |
||||
</bean> |
||||
|
||||
<bean id="casEntryPoint" |
||||
class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"> |
||||
<property name="loginUrl" value="https://localhost:9443/cas/login"/> |
||||
<property name="serviceProperties" ref="serviceProperties"/> |
||||
</bean> |
||||
---- |
||||
|
||||
For CAS to operate, the `ExceptionTranslationFilter` must have its `authenticationEntryPoint` property set to the `CasAuthenticationEntryPoint` bean. |
||||
This can easily be done using xref:servlet/appendix/namespace.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. |
||||
The `CasAuthenticationEntryPoint` must refer to the `ServiceProperties` bean (discussed above), which provides the URL to the enterprise's CAS login server. |
||||
This is where the user's browser will be redirected. |
||||
|
||||
The `CasAuthenticationFilter` has very similar properties to the `UsernamePasswordAuthenticationFilter` (used for form-based logins). |
||||
You can use these properties to customize things like behavior for authentication success and failure. |
||||
|
||||
Next you need to add a `CasAuthenticationProvider` and its collaborators: |
||||
|
||||
[source,xml,attrs="-attributes"] |
||||
---- |
||||
<security:authentication-manager alias="authenticationManager"> |
||||
<security:authentication-provider ref="casAuthenticationProvider" /> |
||||
</security:authentication-manager> |
||||
|
||||
<bean id="casAuthenticationProvider" |
||||
class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> |
||||
<property name="authenticationUserDetailsService"> |
||||
<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> |
||||
<constructor-arg ref="userService" /> |
||||
</bean> |
||||
</property> |
||||
<property name="serviceProperties" ref="serviceProperties" /> |
||||
<property name="ticketValidator"> |
||||
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator"> |
||||
<constructor-arg index="0" value="https://localhost:9443/cas" /> |
||||
</bean> |
||||
</property> |
||||
<property name="key" value="an_id_for_this_auth_provider_only"/> |
||||
</bean> |
||||
|
||||
<security:user-service id="userService"> |
||||
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that |
||||
NoOpPasswordEncoder should be used. |
||||
This is not safe for production, but makes reading |
||||
in samples easier. |
||||
Normally passwords should be hashed using BCrypt --> |
||||
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" /> |
||||
... |
||||
</security:user-service> |
||||
---- |
||||
|
||||
The `CasAuthenticationProvider` uses a `UserDetailsService` instance to load the authorities for a user, once they have been authenticated by CAS. |
||||
We've shown a simple in-memory setup here. |
||||
Note that the `CasAuthenticationProvider` does not actually use the password for authentication, but it does use the authorities. |
||||
|
||||
The beans are all reasonably self-explanatory if you refer back to the <<cas-how-it-works,How CAS Works>> section. |
||||
|
||||
This completes the most basic configuration for CAS. |
||||
If you haven't made any mistakes, your web application should happily work within the framework of CAS single sign on. |
||||
No other parts of Spring Security need to be concerned about the fact CAS handled authentication. |
||||
In the following sections we will discuss some (optional) more advanced configurations. |
||||
|
||||
|
||||
[[cas-singlelogout]] |
||||
=== Single Logout |
||||
The CAS protocol supports Single Logout and can be easily added to your Spring Security configuration. |
||||
Below are updates to the Spring Security configuration that handle Single Logout |
||||
|
||||
[source,xml] |
||||
---- |
||||
<security:http entry-point-ref="casEntryPoint"> |
||||
... |
||||
<security:logout logout-success-url="/cas-logout.jsp"/> |
||||
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/> |
||||
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/> |
||||
</security:http> |
||||
|
||||
<!-- This filter handles a Single Logout Request from the CAS Server --> |
||||
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/> |
||||
|
||||
<!-- This filter redirects to the CAS Server to signal Single Logout should be performed --> |
||||
<bean id="requestSingleLogoutFilter" |
||||
class="org.springframework.security.web.authentication.logout.LogoutFilter"> |
||||
<constructor-arg value="https://localhost:9443/cas/logout"/> |
||||
<constructor-arg> |
||||
<bean class= |
||||
"org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/> |
||||
</constructor-arg> |
||||
<property name="filterProcessesUrl" value="/logout/cas"/> |
||||
</bean> |
||||
---- |
||||
|
||||
The `logout` element logs the user out of the local application, but does not end the session with the CAS server or any other applications that have been logged into. |
||||
The `requestSingleLogoutFilter` filter will allow the URL of `/spring_security_cas_logout` to be requested to redirect the application to the configured CAS Server logout URL. |
||||
Then the CAS Server will send a Single Logout request to all the services that were signed into. |
||||
The `singleLogoutFilter` handles the Single Logout request by looking up the `HttpSession` in a static `Map` and then invalidating it. |
||||
|
||||
It might be confusing why both the `logout` element and the `singleLogoutFilter` are needed. |
||||
It is considered best practice to logout locally first since the `SingleSignOutFilter` just stores the `HttpSession` in a static `Map` in order to call invalidate on it. |
||||
With the configuration above, the flow of logout would be: |
||||
|
||||
* The user requests `/logout` which would log the user out of the local application and send the user to the logout success page. |
||||
* The logout success page, `/cas-logout.jsp`, should instruct the user to click a link pointing to `/logout/cas` in order to logout out of all applications. |
||||
* When the user clicks the link, the user is redirected to the CAS single logout URL (https://localhost:9443/cas/logout). |
||||
* On the CAS Server side, the CAS single logout URL then submits single logout requests to all the CAS Services. |
||||
On the CAS Service side, JASIG's `SingleSignOutFilter` processes the logout request by invalidating the original session. |
||||
|
||||
|
||||
|
||||
The next step is to add the following to your web.xml |
||||
|
||||
[source,xml] |
||||
---- |
||||
<filter> |
||||
<filter-name>characterEncodingFilter</filter-name> |
||||
<filter-class> |
||||
org.springframework.web.filter.CharacterEncodingFilter |
||||
</filter-class> |
||||
<init-param> |
||||
<param-name>encoding</param-name> |
||||
<param-value>UTF-8</param-value> |
||||
</init-param> |
||||
</filter> |
||||
<filter-mapping> |
||||
<filter-name>characterEncodingFilter</filter-name> |
||||
<url-pattern>/*</url-pattern> |
||||
</filter-mapping> |
||||
<listener> |
||||
<listener-class> |
||||
org.jasig.cas.client.session.SingleSignOutHttpSessionListener |
||||
</listener-class> |
||||
</listener> |
||||
---- |
||||
|
||||
When using the SingleSignOutFilter you might encounter some encoding issues. |
||||
Therefore it is recommended to add the `CharacterEncodingFilter` to ensure that the character encoding is correct when using the `SingleSignOutFilter`. |
||||
Again, refer to JASIG's documentation for details. |
||||
The `SingleSignOutHttpSessionListener` ensures that when an `HttpSession` expires, the mapping used for single logout is removed. |
||||
|
||||
|
||||
[[cas-pt-client]] |
||||
=== Authenticating to a Stateless Service with CAS |
||||
This section describes how to authenticate to a service using CAS. |
||||
In other words, this section discusses how to setup a client that uses a service that authenticates with CAS. |
||||
The next section describes how to setup a stateless service to Authenticate using CAS. |
||||
|
||||
|
||||
[[cas-pt-client-config]] |
||||
==== Configuring CAS to Obtain Proxy Granting Tickets |
||||
In order to authenticate to a stateless service, the application needs to obtain a proxy granting ticket (PGT). |
||||
This section describes how to configure Spring Security to obtain a PGT building upon thencas-st[Service Ticket Authentication] configuration. |
||||
|
||||
The first step is to include a `ProxyGrantingTicketStorage` in your Spring Security configuration. |
||||
This is used to store PGT's that are obtained by the `CasAuthenticationFilter` so that they can be used to obtain proxy tickets. |
||||
An example configuration is shown below |
||||
|
||||
[source,xml] |
||||
---- |
||||
<!-- |
||||
NOTE: In a real application you should not use an in memory implementation. |
||||
You will also want to ensure to clean up expired tickets by calling |
||||
ProxyGrantingTicketStorage.cleanup() |
||||
--> |
||||
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/> |
||||
---- |
||||
|
||||
The next step is to update the `CasAuthenticationProvider` to be able to obtain proxy tickets. |
||||
To do this replace the `Cas20ServiceTicketValidator` with a `Cas20ProxyTicketValidator`. |
||||
The `proxyCallbackUrl` should be set to a URL that the application will receive PGT's at. |
||||
Last, the configuration should also reference the `ProxyGrantingTicketStorage` so it can use a PGT to obtain proxy tickets. |
||||
You can find an example of the configuration changes that should be made below. |
||||
|
||||
[source,xml] |
||||
---- |
||||
<bean id="casAuthenticationProvider" |
||||
class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> |
||||
... |
||||
<property name="ticketValidator"> |
||||
<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"> |
||||
<constructor-arg value="https://localhost:9443/cas"/> |
||||
<property name="proxyCallbackUrl" |
||||
value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/> |
||||
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/> |
||||
</bean> |
||||
</property> |
||||
</bean> |
||||
---- |
||||
|
||||
The last step is to update the `CasAuthenticationFilter` to accept PGT and to store them in the `ProxyGrantingTicketStorage`. |
||||
It is important the `proxyReceptorUrl` matches the `proxyCallbackUrl` of the `Cas20ProxyTicketValidator`. |
||||
An example configuration is shown below. |
||||
|
||||
[source,xml] |
||||
---- |
||||
|
||||
<bean id="casFilter" |
||||
class="org.springframework.security.cas.web.CasAuthenticationFilter"> |
||||
... |
||||
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/> |
||||
<property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/> |
||||
</bean> |
||||
|
||||
---- |
||||
|
||||
[[cas-pt-client-sample]] |
||||
==== Calling a Stateless Service Using a Proxy Ticket |
||||
Now that Spring Security obtains PGTs, you can use them to create proxy tickets which can be used to authenticate to a stateless service. |
||||
The CAS xref:samples.adoc#samples[sample application] contains a working example in the `ProxyTicketSampleServlet`. |
||||
Example code can be found below: |
||||
|
||||
==== |
||||
.Java |
||||
[source,java,role="primary"] |
||||
---- |
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) |
||||
throws ServletException, IOException { |
||||
// NOTE: The CasAuthenticationToken can also be obtained using |
||||
// SecurityContextHolder.getContext().getAuthentication() |
||||
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal(); |
||||
// proxyTicket could be reused to make calls to the CAS service even if the |
||||
// target url differs |
||||
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl); |
||||
|
||||
// Make a remote call using the proxy ticket |
||||
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8"); |
||||
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8"); |
||||
... |
||||
} |
||||
---- |
||||
|
||||
.Kotlin |
||||
[source,kotlin,role="secondary"] |
||||
---- |
||||
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) { |
||||
// NOTE: The CasAuthenticationToken can also be obtained using |
||||
// SecurityContextHolder.getContext().getAuthentication() |
||||
val token = request.userPrincipal as CasAuthenticationToken |
||||
// proxyTicket could be reused to make calls to the CAS service even if the |
||||
// target url differs |
||||
val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl) |
||||
|
||||
// Make a remote call using the proxy ticket |
||||
val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8") |
||||
val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8") |
||||
} |
||||
---- |
||||
==== |
||||
|
||||
[[cas-pt]] |
||||
=== Proxy Ticket Authentication |
||||
The `CasAuthenticationProvider` distinguishes between stateful and stateless clients. |
||||
A stateful client is considered any that submits to the `filterProcessUrl` of the `CasAuthenticationFilter`. |
||||
A stateless client is any that presents an authentication request to `CasAuthenticationFilter` on a URL other than the `filterProcessUrl`. |
||||
|
||||
Because remoting protocols have no way of presenting themselves within the context of an `HttpSession`, it isn't possible to rely on the default practice of storing the security context in the session between requests. |
||||
Furthermore, because the CAS server invalidates a ticket after it has been validated by the `TicketValidator`, presenting the same proxy ticket on subsequent requests will not work. |
||||
|
||||
One obvious option is to not use CAS at all for remoting protocol clients. |
||||
However, this would eliminate many of the desirable features of CAS. |
||||
As a middle-ground, the `CasAuthenticationProvider` uses a `StatelessTicketCache`. |
||||
This is used solely for stateless clients which use a principal equal to `CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER`. |
||||
What happens is the `CasAuthenticationProvider` will store the resulting `CasAuthenticationToken` in the `StatelessTicketCache`, keyed on the proxy ticket. |
||||
Accordingly, remoting protocol clients can present the same proxy ticket and the `CasAuthenticationProvider` will not need to contact the CAS server for validation (aside from the first request). |
||||
Once authenticated, the proxy ticket could be used for URLs other than the original target service. |
||||
|
||||
This section builds upon the previous sections to accommodate proxy ticket authentication. |
||||
The first step is to specify to authenticate all artifacts as shown below. |
||||
|
||||
[source,xml] |
||||
---- |
||||
<bean id="serviceProperties" |
||||
class="org.springframework.security.cas.ServiceProperties"> |
||||
... |
||||
<property name="authenticateAllArtifacts" value="true"/> |
||||
</bean> |
||||
---- |
||||
|
||||
The next step is to specify `serviceProperties` and the `authenticationDetailsSource` for the `CasAuthenticationFilter`. |
||||
The `serviceProperties` property instructs the `CasAuthenticationFilter` to attempt to authenticate all artifacts instead of only ones present on the `filterProcessUrl`. |
||||
The `ServiceAuthenticationDetailsSource` creates a `ServiceAuthenticationDetails` that ensures the current URL, based upon the `HttpServletRequest`, is used as the service URL when validating the ticket. |
||||
The method for generating the service URL can be customized by injecting a custom `AuthenticationDetailsSource` that returns a custom `ServiceAuthenticationDetails`. |
||||
|
||||
[source,xml] |
||||
---- |
||||
<bean id="casFilter" |
||||
class="org.springframework.security.cas.web.CasAuthenticationFilter"> |
||||
... |
||||
<property name="serviceProperties" ref="serviceProperties"/> |
||||
<property name="authenticationDetailsSource"> |
||||
<bean class= |
||||
"org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"> |
||||
<constructor-arg ref="serviceProperties"/> |
||||
</bean> |
||||
</property> |
||||
</bean> |
||||
---- |
||||
|
||||
You will also need to update the `CasAuthenticationProvider` to handle proxy tickets. |
||||
To do this replace the `Cas20ServiceTicketValidator` with a `Cas20ProxyTicketValidator`. |
||||
You will need to configure the `statelessTicketCache` and which proxies you want to accept. |
||||
You can find an example of the updates required to accept all proxies below. |
||||
|
||||
[source,xml] |
||||
---- |
||||
|
||||
<bean id="casAuthenticationProvider" |
||||
class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> |
||||
... |
||||
<property name="ticketValidator"> |
||||
<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"> |
||||
<constructor-arg value="https://localhost:9443/cas"/> |
||||
<property name="acceptAnyProxy" value="true"/> |
||||
</bean> |
||||
</property> |
||||
<property name="statelessTicketCache"> |
||||
<bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache"> |
||||
<property name="cache"> |
||||
<bean class="net.sf.ehcache.Cache" |
||||
init-method="initialise" destroy-method="dispose"> |
||||
<constructor-arg value="casTickets"/> |
||||
<constructor-arg value="50"/> |
||||
<constructor-arg value="true"/> |
||||
<constructor-arg value="false"/> |
||||
<constructor-arg value="3600"/> |
||||
<constructor-arg value="900"/> |
||||
</bean> |
||||
</property> |
||||
</bean> |
||||
</property> |
||||
</bean> |
||||
---- |
||||
Loading…
Reference in new issue