mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 01:02:14 +00:00
Revert "Remove CAS module"
This reverts commit caf4c471
This commit is contained in:
parent
baf2c98c9f
commit
b4d3ac6665
25
cas/spring-security-cas.gradle
Normal file
25
cas/spring-security-cas.gradle
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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'
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,397 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,382 @@
|
|||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
cas/src/test/resources/logback-test.xml
Normal file
15
cas/src/test/resources/logback-test.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
@ -0,0 +1,23 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,15 @@
|
|||||||
|
<?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>
|
@ -41,6 +41,7 @@ dependencies {
|
|||||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||||
|
|
||||||
testImplementation project(':spring-security-aspects')
|
testImplementation project(':spring-security-aspects')
|
||||||
|
testImplementation project(':spring-security-cas')
|
||||||
testImplementation project(':spring-security-test')
|
testImplementation project(':spring-security-test')
|
||||||
testImplementation project(path : ':spring-security-core', configuration : 'tests')
|
testImplementation project(path : ':spring-security-core', configuration : 'tests')
|
||||||
testImplementation project(path : ':spring-security-ldap', configuration : 'tests')
|
testImplementation project(path : ':spring-security-ldap', configuration : 'tests')
|
||||||
|
@ -20,6 +20,8 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -29,6 +31,8 @@ import org.springframework.beans.factory.BeanCreationException;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.cas.web.CasAuthenticationFilter;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.test.SpringTestContext;
|
import org.springframework.security.config.test.SpringTestContext;
|
||||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
import org.springframework.security.config.test.SpringTestContextExtension;
|
||||||
@ -42,6 +46,9 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@ -68,6 +75,16 @@ public class HttpConfigurationTests {
|
|||||||
+ " Consider using addFilterBefore or addFilterAfter instead.");
|
+ " Consider using addFilterBefore or addFilterAfter instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/spring-projects/spring-security-javaconfig/issues/104
|
||||||
|
@Test
|
||||||
|
public void configureWhenAddFilterCasAuthenticationFilterThenFilterAdded() throws Exception {
|
||||||
|
CasAuthenticationFilterConfig.CAS_AUTHENTICATION_FILTER = spy(new CasAuthenticationFilter());
|
||||||
|
this.spring.register(CasAuthenticationFilterConfig.class).autowire();
|
||||||
|
this.mockMvc.perform(get("/"));
|
||||||
|
verify(CasAuthenticationFilterConfig.CAS_AUTHENTICATION_FILTER).doFilter(any(ServletRequest.class),
|
||||||
|
any(ServletResponse.class), any(FilterChain.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void configureWhenConfigIsRequestMatchersJavadocThenAuthorizationApplied() throws Exception {
|
public void configureWhenConfigIsRequestMatchersJavadocThenAuthorizationApplied() throws Exception {
|
||||||
this.spring.register(RequestMatcherRegistryConfigs.class).autowire();
|
this.spring.register(RequestMatcherRegistryConfigs.class).autowire();
|
||||||
@ -107,6 +124,21 @@ public class HttpConfigurationTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnableWebSecurity
|
||||||
|
static class CasAuthenticationFilterConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
|
static CasAuthenticationFilter CAS_AUTHENTICATION_FILTER;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.addFilter(CAS_AUTHENTICATION_FILTER);
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
*** xref:servlet/authentication/anonymous.adoc[Anonymous]
|
*** xref:servlet/authentication/anonymous.adoc[Anonymous]
|
||||||
*** xref:servlet/authentication/preauth.adoc[Pre-Authentication]
|
*** xref:servlet/authentication/preauth.adoc[Pre-Authentication]
|
||||||
*** xref:servlet/authentication/jaas.adoc[JAAS]
|
*** xref:servlet/authentication/jaas.adoc[JAAS]
|
||||||
|
*** xref:servlet/authentication/cas.adoc[CAS]
|
||||||
*** xref:servlet/authentication/x509.adoc[X509]
|
*** xref:servlet/authentication/x509.adoc[X509]
|
||||||
*** xref:servlet/authentication/runas.adoc[Run-As]
|
*** xref:servlet/authentication/runas.adoc[Run-As]
|
||||||
*** xref:servlet/authentication/logout.adoc[Logout]
|
*** xref:servlet/authentication/logout.adoc[Logout]
|
||||||
|
463
docs/modules/ROOT/pages/servlet/authentication/cas.adoc
Normal file
463
docs/modules/ROOT/pages/servlet/authentication/cas.adoc
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
[[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>
|
||||||
|
----
|
@ -16,6 +16,7 @@ These sections focus on specific ways you may want to authenticate and point bac
|
|||||||
* xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd[Username and Password] - how to authenticate with a username/password
|
* xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd[Username and Password] - how to authenticate with a username/password
|
||||||
* xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub)
|
* xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub)
|
||||||
* xref:servlet/saml2/index.adoc#servlet-saml2[SAML 2.0 Login] - SAML 2.0 Log In
|
* xref:servlet/saml2/index.adoc#servlet-saml2[SAML 2.0 Login] - SAML 2.0 Log In
|
||||||
|
* xref:servlet/authentication/cas.adoc#servlet-cas[Central Authentication Server (CAS)] - Central Authentication Server (CAS) Support
|
||||||
* xref:servlet/authentication/rememberme.adoc#servlet-rememberme[Remember Me] - how to remember a user past session expiration
|
* xref:servlet/authentication/rememberme.adoc#servlet-rememberme[Remember Me] - how to remember a user past session expiration
|
||||||
* xref:servlet/authentication/jaas.adoc#servlet-jaas[JAAS Authentication] - authenticate with JAAS
|
* xref:servlet/authentication/jaas.adoc#servlet-jaas[JAAS Authentication] - authenticate with JAAS
|
||||||
* xref:servlet/authentication/preauth.adoc#servlet-preauth[Pre-Authentication Scenarios] - authenticate with an external mechanism such as https://www.siteminder.com/[SiteMinder] or Java EE security but still use Spring Security for authorization and protection against common exploits.
|
* xref:servlet/authentication/preauth.adoc#servlet-preauth[Pre-Authentication Scenarios] - authenticate with an external mechanism such as https://www.siteminder.com/[SiteMinder] or Java EE security but still use Spring Security for authorization and protection against common exploits.
|
||||||
|
@ -145,5 +145,6 @@ If not configured, a status code 200 is returned by default.
|
|||||||
- xref:servlet/test/mockmvc/logout.adoc#test-logout[Testing Logout]
|
- xref:servlet/test/mockmvc/logout.adoc#test-logout[Testing Logout]
|
||||||
- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[`HttpServletRequest.logout()`]
|
- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[`HttpServletRequest.logout()`]
|
||||||
- xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations]
|
- xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations]
|
||||||
|
- Documentation for the xref:servlet/appendix/namespace.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section
|
||||||
- xref:servlet/exploits/csrf.adoc#servlet-considerations-csrf-logout[Logging Out] in section CSRF Caveats
|
- xref:servlet/exploits/csrf.adoc#servlet-considerations-csrf-logout[Logging Out] in section CSRF Caveats
|
||||||
- Documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[logout element] in the Spring Security XML Namespace section
|
- Documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[logout element] in the Spring Security XML Namespace section
|
||||||
|
Loading…
x
Reference in New Issue
Block a user