mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-30 08:42:13 +00:00
Use a Custom Authentication Token for CAS
Closes gh-12304
This commit is contained in:
parent
e0284a4503
commit
04369cf2da
@ -30,9 +30,7 @@ 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;
|
||||
@ -51,11 +49,11 @@ import org.springframework.util.Assert;
|
||||
* Authentication Service (CAS).
|
||||
* <p>
|
||||
* This <code>AuthenticationProvider</code> is capable of validating
|
||||
* {@link UsernamePasswordAuthenticationToken} requests which contain a
|
||||
* {@link CasServiceTicketAuthenticationToken} 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}.
|
||||
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} or
|
||||
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER}. It can also
|
||||
* validate a previously created {@link CasAuthenticationToken}.
|
||||
*
|
||||
* @author Ben Alex
|
||||
* @author Scott Battaglia
|
||||
@ -95,13 +93,6 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
|
||||
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()) {
|
||||
@ -117,8 +108,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
|
||||
"Failed to provide a CAS service ticket to validate"));
|
||||
}
|
||||
|
||||
boolean stateless = (authentication instanceof UsernamePasswordAuthenticationToken
|
||||
&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication.getPrincipal()));
|
||||
boolean stateless = (authentication instanceof CasServiceTicketAuthenticationToken token && token.isStateless());
|
||||
CasAuthenticationToken result = null;
|
||||
|
||||
if (stateless) {
|
||||
@ -236,7 +226,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
|
||||
|
||||
@Override
|
||||
public boolean supports(final Class<?> authentication) {
|
||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication))
|
||||
return (CasServiceTicketAuthenticationToken.class.isAssignableFrom(authentication))
|
||||
|| (CasAuthenticationToken.class.isAssignableFrom(authentication))
|
||||
|| (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication));
|
||||
}
|
||||
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.io.Serial;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that is
|
||||
* designed to process CAS service ticket.
|
||||
*
|
||||
* @author Hal Deadman
|
||||
* @since 6.1
|
||||
*/
|
||||
public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
|
||||
|
||||
static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
private final String identifier;
|
||||
|
||||
private Object credentials;
|
||||
|
||||
/**
|
||||
* This constructor can be safely used by any code that wishes to create a
|
||||
* <code>CasServiceTicketAuthenticationToken</code>, as the {@link #isAuthenticated()}
|
||||
* will return <code>false</code>.
|
||||
*
|
||||
*/
|
||||
public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
|
||||
super(null);
|
||||
this.identifier = identifier;
|
||||
this.credentials = credentials;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This constructor should only be used by <code>AuthenticationManager</code> or
|
||||
* <code>AuthenticationProvider</code> implementations that are satisfied with
|
||||
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
|
||||
* authentication token.
|
||||
* @param identifier
|
||||
* @param credentials
|
||||
* @param authorities
|
||||
*/
|
||||
public CasServiceTicketAuthenticationToken(String identifier, Object credentials,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.identifier = identifier;
|
||||
this.credentials = credentials;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
|
||||
return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
|
||||
}
|
||||
|
||||
public static CasServiceTicketAuthenticationToken stateless(Object credentials) {
|
||||
return new CasServiceTicketAuthenticationToken(CAS_STATELESS_IDENTIFIER, credentials);
|
||||
}
|
||||
|
||||
public boolean isStateless() {
|
||||
return CAS_STATELESS_IDENTIFIER.equals(this.identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.identifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||
Assert.isTrue(!isAuthenticated,
|
||||
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
|
||||
super.setAuthenticated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
super.eraseCredentials();
|
||||
this.credentials = null;
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
* Copyright 2002-2023 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.
|
||||
@ -26,6 +26,7 @@ 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.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@ -72,7 +73,8 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
|
||||
String urlEncodedService = createServiceUrl(servletRequest, response);
|
||||
String redirectUrl = createRedirectUrl(urlEncodedService);
|
||||
preCommence(servletRequest, response);
|
||||
response.sendRedirect(redirectUrl);
|
||||
new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl);
|
||||
// response.sendRedirect(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
* Copyright 2002-2023 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.
|
||||
@ -29,9 +29,9 @@ import org.apereo.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.authentication.CasServiceTicketAuthenticationToken;
|
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
|
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@ -41,6 +41,7 @@ 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.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
@ -63,9 +64,9 @@ import org.springframework.util.Assert;
|
||||
* <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>.
|
||||
* <code>CasServiceTicketAuthenticationToken</code> which uses
|
||||
* {@link CasServiceTicketAuthenticationToken#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
|
||||
@ -88,15 +89,15 @@ import org.springframework.util.Assert;
|
||||
* {@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>CasServiceTicketAuthenticationToken</code> which uses
|
||||
* {@link CasServiceTicketAuthenticationToken#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
|
||||
* that can recognise <code>CasServiceTicketAuthenticationToken</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
|
||||
@ -175,19 +176,6 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -207,6 +195,7 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
public CasAuthenticationFilter() {
|
||||
super("/login/cas");
|
||||
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
|
||||
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -238,14 +227,15 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
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) {
|
||||
String serviceTicket = obtainArtifact(request);
|
||||
if (serviceTicket == null) {
|
||||
this.logger.debug("Failed to obtain an artifact (cas ticket)");
|
||||
password = "";
|
||||
serviceTicket = "";
|
||||
}
|
||||
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
|
||||
boolean serviceTicketRequest = serviceTicketRequest(request, response);
|
||||
CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
|
||||
? CasServiceTicketAuthenticationToken.stateful(serviceTicket)
|
||||
: CasServiceTicketAuthenticationToken.stateless(serviceTicket);
|
||||
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
return this.getAuthenticationManager().authenticate(authRequest);
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ 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;
|
||||
@ -87,8 +86,7 @@ public class CasAuthenticationProviderTests {
|
||||
cap.setServiceProperties(makeServiceProperties());
|
||||
cap.setTicketValidator(new MockTicketValidator(true));
|
||||
cap.afterPropertiesSet();
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123");
|
||||
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123");
|
||||
token.setDetails("details");
|
||||
Authentication result = cap.authenticate(token);
|
||||
// Confirm ST-123 was NOT added to the cache
|
||||
@ -120,8 +118,7 @@ public class CasAuthenticationProviderTests {
|
||||
cap.setTicketValidator(new MockTicketValidator(true));
|
||||
cap.setServiceProperties(makeServiceProperties());
|
||||
cap.afterPropertiesSet();
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456");
|
||||
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("ST-456");
|
||||
token.setDetails("details");
|
||||
Authentication result = cap.authenticate(token);
|
||||
// Confirm ST-456 was added to the cache
|
||||
@ -135,7 +132,7 @@ public class CasAuthenticationProviderTests {
|
||||
// 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
|
||||
// Previously created CasServiceTicketAuthenticationToken is OK
|
||||
Authentication newResult = cap.authenticate(token);
|
||||
assertThat(newResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator());
|
||||
assertThat(newResult.getCredentials()).isEqualTo("ST-456");
|
||||
@ -157,8 +154,7 @@ public class CasAuthenticationProviderTests {
|
||||
cap.setServiceProperties(serviceProperties);
|
||||
cap.afterPropertiesSet();
|
||||
String ticket = "ST-456";
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
|
||||
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
|
||||
Authentication result = cap.authenticate(token);
|
||||
}
|
||||
|
||||
@ -178,8 +174,7 @@ public class CasAuthenticationProviderTests {
|
||||
cap.setServiceProperties(serviceProperties);
|
||||
cap.afterPropertiesSet();
|
||||
String ticket = "ST-456";
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
|
||||
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
|
||||
Authentication result = cap.authenticate(token);
|
||||
verify(validator).validate(ticket, serviceProperties.getService());
|
||||
serviceProperties.setAuthenticateAllArtifacts(true);
|
||||
@ -211,8 +206,7 @@ public class CasAuthenticationProviderTests {
|
||||
cap.setTicketValidator(new MockTicketValidator(true));
|
||||
cap.setServiceProperties(makeServiceProperties());
|
||||
cap.afterPropertiesSet();
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "");
|
||||
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("");
|
||||
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token));
|
||||
}
|
||||
|
||||
@ -322,7 +316,7 @@ public class CasAuthenticationProviderTests {
|
||||
@Test
|
||||
public void supportsRequiredTokens() {
|
||||
CasAuthenticationProvider cap = new CasAuthenticationProvider();
|
||||
assertThat(cap.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
|
||||
assertThat(cap.supports(CasServiceTicketAuthenticationToken.class)).isTrue();
|
||||
assertThat(cap.supports(CasAuthenticationToken.class)).isTrue();
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ import org.apereo.cas.client.validation.Assertion;
|
||||
import org.apereo.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;
|
||||
@ -116,16 +115,6 @@ public class CasAuthenticationTokenTests {
|
||||
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");
|
||||
|
Loading…
x
Reference in New Issue
Block a user