diff --git a/README.md b/README.md index 7283a1e..b51fabb 100644 --- a/README.md +++ b/README.md @@ -202,10 +202,22 @@ The `AuthenticationFilter` is what detects whether a user needs to be authentica | `serviceParameterName ` | specifies the name of the request parameter on where to find the service (i.e. `service`) | No | `encodeServiceUrl ` | Whether the client should auto encode the service url. Defaults to `true` | No | `ignorePattern` | Defines the url pattern to ignore, when intercepting authentication requests. | No -| `ignoreUrlPatternType` | Defines the type of the pattern specified. Defaults to `REGEX`. Other types are `CONTAINS`, `EXACT`. | No +| `ignoreUrlPatternType` | Defines the type of the pattern specified. Defaults to `REGEX`. Other types are `CONTAINS`, `EXACT`, `FULL_REGEX`. Can also accept a fully-qualified class name that implements `UrlPatternMatcherStrategy`. | No | `gatewayStorageClass` | The storage class used to record gateway requests | No | `authenticationRedirectStrategyClass` | The class name of the component to decide how to handle authn redirects to CAS | No +##### Ignore Patterns + +The following types are supported: + +| Type | Description +|----------|------- +| `REGEX` | Matches the URL the `ignorePattern` using `Matcher#find()`. It matches the next occurrence within the substring that matches the regex. +| `CONTAINS` | Uses the `String#contains()` operation to determine if the url contains the specified pattern. Behavior is case-sensitive. +| `EXACT` | Uses the `String#equals()` operation to determine if the url exactly equals the specified pattern. Behavior is case-sensitive. +| `FULL_REGEX` | Matches the URL the `ignorePattern` using `Matcher#matches()`. It matches the expression against the entire string as it implicitly add a `^` at the start and `$` at the end of the pattern, so it will not match substring or part of the string. `^` and `$` are meta characters that represents start of the string and end of the string respectively. + + #### org.jasig.cas.client.authentication.Saml11AuthenticationFilter The SAML 1.1 `AuthenticationFilter` is what detects whether a user needs to be authenticated or not. If a user needs to be authenticated, it will redirect the user to the CAS server. diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java index 07400ef..75cdc0c 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java @@ -18,15 +18,6 @@ */ package org.jasig.cas.client.authentication; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - import org.jasig.cas.client.Protocol; import org.jasig.cas.client.configuration.ConfigurationKeys; import org.jasig.cas.client.util.AbstractCasFilter; @@ -34,6 +25,18 @@ import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.util.ReflectUtils; import org.jasig.cas.client.validation.Assertion; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + /** * Filter implementation to intercept all requests and attempt to authenticate * the user by redirecting them to CAS (unless the user has a ticket). @@ -70,15 +73,16 @@ public class AuthenticationFilter extends AbstractCasFilter { private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl(); private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy(); - + private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null; - + private static final Map> PATTERN_MATCHER_TYPES = - new HashMap>(); - + new HashMap>(); + static { PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class); PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class); + PATTERN_MATCHER_TYPES.put("FULL_REGEX", EntireRegionRegexUrlPatternMatcherStrategy.class); PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class); } @@ -89,7 +93,7 @@ public class AuthenticationFilter extends AbstractCasFilter { protected AuthenticationFilter(final Protocol protocol) { super(protocol); } - + protected void initInternal(final FilterConfig filterConfig) throws ServletException { if (!isIgnoreInitConfiguration()) { super.initInternal(filterConfig); @@ -103,10 +107,10 @@ public class AuthenticationFilter extends AbstractCasFilter { setRenew(getBoolean(ConfigurationKeys.RENEW)); setGateway(getBoolean(ConfigurationKeys.GATEWAY)); - + final String ignorePattern = getString(ConfigurationKeys.IGNORE_PATTERN); final String ignoreUrlPatternType = getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE); - + if (ignorePattern != null) { final Class ignoreUrlMatcherClass = PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType); if (ignoreUrlMatcherClass != null) { @@ -123,13 +127,13 @@ public class AuthenticationFilter extends AbstractCasFilter { this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern); } } - + final Class gatewayStorageClass = getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS); if (gatewayStorageClass != null) { setGatewayStorage(ReflectUtils.newInstance(gatewayStorageClass)); } - + final Class authenticationRedirectStrategyClass = getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS); if (authenticationRedirectStrategyClass != null) { @@ -150,17 +154,17 @@ public class AuthenticationFilter extends AbstractCasFilter { } public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, - final FilterChain filterChain) throws IOException, ServletException { - + final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; - + if (isRequestUrlExcluded(request)) { logger.debug("Request is ignored."); filterChain.doFilter(request, response); return; } - + final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; @@ -191,7 +195,7 @@ public class AuthenticationFilter extends AbstractCasFilter { logger.debug("Constructed service url: {}", modifiedServiceUrl); final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, - getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); + getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); logger.debug("redirecting to \"{}\"", urlToRedirectTo); this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo); @@ -216,12 +220,12 @@ public class AuthenticationFilter extends AbstractCasFilter { public final void setGatewayStorage(final GatewayResolver gatewayStorage) { this.gatewayStorage = gatewayStorage; } - + private boolean isRequestUrlExcluded(final HttpServletRequest request) { if (this.ignoreUrlPatternMatcherStrategyClass == null) { return false; } - + final StringBuffer urlBuffer = request.getRequestURL(); if (request.getQueryString() != null) { urlBuffer.append("?").append(request.getQueryString()); @@ -231,7 +235,7 @@ public class AuthenticationFilter extends AbstractCasFilter { } public final void setIgnoreUrlPatternMatcherStrategyClass( - final UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass) { + final UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass) { this.ignoreUrlPatternMatcherStrategyClass = ignoreUrlPatternMatcherStrategyClass; } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/EntireRegionRegexUrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/EntireRegionRegexUrlPatternMatcherStrategy.java new file mode 100644 index 0000000..af35b74 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/EntireRegionRegexUrlPatternMatcherStrategy.java @@ -0,0 +1,51 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you 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 the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.authentication; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A pattern matcher that looks inside the url to find the pattern, that + * is assumed to have been specified via regular expressions syntax. + * The match behavior is based on {@link Matcher#matches()}: + * Attempts to match the entire region against the pattern. + * + * @author Misagh Moayyed + * @since 3.5 + */ +public final class EntireRegionRegexUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy { + + private Pattern pattern; + + public EntireRegionRegexUrlPatternMatcherStrategy() { + } + + public EntireRegionRegexUrlPatternMatcherStrategy(final String pattern) { + this.setPattern(pattern); + } + + public boolean matches(final String url) { + return this.pattern.matcher(url).matches(); + } + + public void setPattern(final String pattern) { + this.pattern = Pattern.compile(pattern); + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java index e5665cd..eae72a3 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java @@ -18,12 +18,19 @@ */ package org.jasig.cas.client.authentication; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * A pattern matcher that looks inside the url to find the pattern,. that + * A pattern matcher that looks inside the url to find the pattern, that * is assumed to have been specified via regular expressions syntax. - * + * The match behavior is based on {@link Matcher#find()}: + * Attempts to find the next subsequence of the input sequence that matches + * the pattern. This method starts at the beginning of this matcher's region, or, if + * a previous invocation of the method was successful and the matcher has + * not since been reset, at the first character not matched by the previous + * match. + * * @author Misagh Moayyed * @since 3.3.1 */ @@ -31,12 +38,13 @@ public final class RegexUrlPatternMatcherStrategy implements UrlPatternMatcherSt private Pattern pattern; - public RegexUrlPatternMatcherStrategy() {} + public RegexUrlPatternMatcherStrategy() { + } public RegexUrlPatternMatcherStrategy(final String pattern) { this.setPattern(pattern); } - + public boolean matches(final String url) { return this.pattern.matcher(url).find(); } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java index 214d787..77e74ce 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java @@ -18,24 +18,27 @@ */ package org.jasig.cas.client.authentication; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.URL; -import java.net.URLEncoder; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.validation.AssertionImpl; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.web.*; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLEncoder; + +import static org.junit.Assert.*; /** * Tests for the AuthenticationFilter. @@ -100,7 +103,7 @@ public final class AuthenticationFilterTests { this.filter.doFilter(request, response, filterChain); assertEquals(CAS_LOGIN_URL + "?service=" + URLEncoder.encode(CAS_SERVICE_URL, "UTF-8"), - response.getRedirectedUrl()); + response.getRedirectedUrl()); } @Test @@ -129,11 +132,11 @@ public final class AuthenticationFilterTests { this.filter.doFilter(request, response, filterChain); assertEquals( - CAS_LOGIN_URL - + "?service=" - + URLEncoder.encode( - "https://localhost:8443" + request.getRequestURI() + "?" + request.getQueryString(), - "UTF-8"), response.getRedirectedUrl()); + CAS_LOGIN_URL + + "?service=" + + URLEncoder.encode( + "https://localhost:8443" + request.getRequestURI() + "?" + request.getQueryString(), + "UTF-8"), response.getRedirectedUrl()); } @Test @@ -198,7 +201,7 @@ public final class AuthenticationFilterTests { this.filter.doFilter(request, response2, filterChain); assertNotNull(session.getAttribute(DefaultGatewayResolverImpl.CONST_CAS_GATEWAY)); assertNull(response2.getRedirectedUrl()); - + final MockHttpServletResponse response3 = new MockHttpServletResponse(); this.filter.doFilter(request, response3, filterChain); assertNotNull(session.getAttribute(DefaultGatewayResolverImpl.CONST_CAS_GATEWAY)); @@ -240,27 +243,27 @@ public final class AuthenticationFilterTests { context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); context.addInitParameter("service", CAS_SERVICE_URL); context.addInitParameter("authenticationRedirectStrategyClass", - "org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy"); + "org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy"); f.init(new MockFilterConfig(context)); } - + @Test public void testIgnorePatterns() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - + context.addInitParameter("ignorePattern", "=valueTo(\\w+)"); context.addInitParameter("service", CAS_SERVICE_URL); f.init(new MockFilterConfig(context)); - + final MockHttpServletRequest request = new MockHttpServletRequest(); final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; request.setRequestURI(URL); - + final MockHttpSession session = new MockHttpSession(); request.setSession(session); - + final MockHttpServletResponse response = new MockHttpServletResponse(); final FilterChain filterChain = new FilterChain() { @@ -271,25 +274,25 @@ public final class AuthenticationFilterTests { f.doFilter(request, response, filterChain); assertNull(response.getRedirectedUrl()); } - + @Test public void testIgnorePatternsWithContainsMatching() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - + context.addInitParameter("ignorePattern", "=valueToIgnore"); context.addInitParameter("ignoreUrlPatternType", "CONTAINS"); context.addInitParameter("service", CAS_SERVICE_URL); f.init(new MockFilterConfig(context)); - + final MockHttpServletRequest request = new MockHttpServletRequest(); final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; request.setRequestURI(URL); - + final MockHttpSession session = new MockHttpSession(); request.setSession(session); - + final MockHttpServletResponse response = new MockHttpServletResponse(); final FilterChain filterChain = new FilterChain() { @@ -300,30 +303,30 @@ public final class AuthenticationFilterTests { f.doFilter(request, response, filterChain); assertNull(response.getRedirectedUrl()); } - + @Test public void testIgnorePatternsWithExactMatching() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - + final URL url = new URL(CAS_SERVICE_URL + "?param=valueToIgnore"); - + context.addInitParameter("ignorePattern", url.toExternalForm()); context.addInitParameter("ignoreUrlPatternType", "EXACT"); context.addInitParameter("service", CAS_SERVICE_URL); f.init(new MockFilterConfig(context)); - + final MockHttpServletRequest request = new MockHttpServletRequest(); request.setScheme(url.getProtocol()); request.setServerName(url.getHost()); request.setServerPort(url.getPort()); request.setQueryString(url.getQuery()); request.setRequestURI(url.getPath()); - + final MockHttpSession session = new MockHttpSession(); request.setSession(session); - + final MockHttpServletResponse response = new MockHttpServletResponse(); final FilterChain filterChain = new FilterChain() { @@ -334,25 +337,25 @@ public final class AuthenticationFilterTests { f.doFilter(request, response, filterChain); assertNull(response.getRedirectedUrl()); } - + @Test public void testIgnorePatternsWithExactClassname() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - + context.addInitParameter("ignorePattern", "=valueToIgnore"); context.addInitParameter("ignoreUrlPatternType", ContainsPatternUrlPatternMatcherStrategy.class.getName()); context.addInitParameter("service", CAS_SERVICE_URL); f.init(new MockFilterConfig(context)); - + final MockHttpServletRequest request = new MockHttpServletRequest(); final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; request.setRequestURI(URL); - + final MockHttpSession session = new MockHttpSession(); request.setSession(session); - + final MockHttpServletResponse response = new MockHttpServletResponse(); final FilterChain filterChain = new FilterChain() { @@ -363,25 +366,25 @@ public final class AuthenticationFilterTests { f.doFilter(request, response, filterChain); assertNull(response.getRedirectedUrl()); } - + @Test public void testIgnorePatternsWithInvalidClassname() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - + context.addInitParameter("ignorePattern", "=valueToIgnore"); context.addInitParameter("ignoreUrlPatternType", "unknown.class.name"); context.addInitParameter("service", CAS_SERVICE_URL); f.init(new MockFilterConfig(context)); - + final MockHttpServletRequest request = new MockHttpServletRequest(); final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; request.setRequestURI(URL); - + final MockHttpSession session = new MockHttpSession(); request.setSession(session); - + final MockHttpServletResponse response = new MockHttpServletResponse(); final FilterChain filterChain = new FilterChain() {