1
0
mirror of synced 2026-08-04 17:27:13 +00:00

SEC-1125: Created separate web module spring-security-web

This commit is contained in:
Luke Taylor
2009-03-25 06:28:18 +00:00
parent 2c985a1c36
commit 2a9a8a41db
247 changed files with 611 additions and 506 deletions
+57
View File
@@ -0,0 +1,57 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-parent</artifactId>
<version>2.5.0-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>
<artifactId>spring-security-web</artifactId>
<name>Spring Security - Web Application Security Module</name>
<dependencies>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.web</artifactId>
<!-- optional>true</optional -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.test</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,131 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.concurrent;
import org.springframework.security.Authentication;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.ui.logout.LogoutHandler;
import org.springframework.security.ui.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* Filter required by concurrent session handling package.
* <p>
* This filter performs two functions. First, it calls
* {@link org.springframework.security.concurrent.SessionRegistry#refreshLastRequest(String)} for each request
* so that registered sessions always have a correct "last update" date/time. Second, it retrieves a
* {@link org.springframework.security.concurrent.SessionInformation} from the <code>SessionRegistry</code>
* for each request and checks if the session has been marked as expired.
* If it has been marked as expired, the configured logout handlers will be called (as happens with
* {@link org.springframework.security.ui.logout.LogoutFilter}), typically to invalidate the session.
* A redirect to the expiredURL specified will be performed, and the session invalidation will cause an
* {@link org.springframework.security.ui.session.HttpSessionDestroyedEvent} to be published via the
* {@link org.springframework.security.ui.session.HttpSessionEventPublisher} registered in <code>web.xml</code>.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class ConcurrentSessionFilter extends SpringSecurityFilter implements InitializingBean {
//~ Instance fields ================================================================================================
private SessionRegistry sessionRegistry;
private String expiredUrl;
private LogoutHandler[] handlers = new LogoutHandler[] {new SecurityContextLogoutHandler()};
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(sessionRegistry, "SessionRegistry required");
Assert.isTrue(expiredUrl == null || UrlUtils.isValidRedirectUrl(expiredUrl),
expiredUrl + " isn't a valid redirect URL");
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = sessionRegistry.getSessionInformation(session.getId());
if (info != null) {
if (info.isExpired()) {
// Expired - abort processing
doLogout(request, response);
String targetUrl = determineExpiredUrl(request, info);
if (targetUrl != null) {
targetUrl = request.getContextPath() + targetUrl;
response.sendRedirect(response.encodeRedirectURL(targetUrl));
} else {
response.getWriter().print("This session has been expired (possibly due to multiple concurrent " +
"logins being attempted as the same user).");
response.flushBuffer();
}
return;
} else {
// Non-expired - update last request date/time
info.refreshLastRequest();
}
}
}
chain.doFilter(request, response);
}
protected String determineExpiredUrl(HttpServletRequest request, SessionInformation info) {
return expiredUrl;
}
private void doLogout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
for (int i = 0; i < handlers.length; i++) {
handlers[i].logout(request, response, auth);
}
}
public void setExpiredUrl(String expiredUrl) {
this.expiredUrl = expiredUrl;
}
public void setSessionRegistry(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
public void setLogoutHandlers(LogoutHandler[] handlers) {
Assert.notNull(handlers);
this.handlers = handlers;
}
public int getOrder() {
return FilterChainOrder.CONCURRENT_SESSION_FILTER;
}
}
@@ -0,0 +1,177 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.concurrent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.security.ui.session.HttpSessionDestroyedEvent;
import org.springframework.util.Assert;
/**
* Base implementation of {@link org.springframework.security.concurrent.SessionRegistry}
* which also listens for {@link org.springframework.security.ui.session.HttpSessionDestroyedEvent}s
* published in the Spring application context.
*
* <p>
* NB: It is important that you register the {@link org.springframework.security.ui.session.HttpSessionEventPublisher} in
* <code>web.xml</code> so that this class is notified of sessions that expire.
* </p>
*
* @author Ben Alex
* @version $Id$
*/
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener {
//~ Static fields/initializers =====================================================================================
protected static final Log logger = LogFactory.getLog(SessionRegistryImpl.class);
// ~ Instance fields ===============================================================================================
/** <principal:Object,SessionIdSet> */
private Map<Object,Set<String>> principals = Collections.synchronizedMap(new HashMap<Object,Set<String>>());
/** <sessionId:Object,SessionInformation> */
private Map<String, SessionInformation> sessionIds = Collections.synchronizedMap(new HashMap<String, SessionInformation>());
// ~ Methods =======================================================================================================
public Object[] getAllPrincipals() {
return principals.keySet().toArray();
}
public SessionInformation[] getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return null;
}
List<SessionInformation> list = new ArrayList<SessionInformation>();
synchronized (sessionsUsedByPrincipal) {
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);
if (sessionInformation == null) {
continue;
}
if (includeExpiredSessions || !sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
}
return (SessionInformation[]) list.toArray(new SessionInformation[0]);
}
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
return (SessionInformation) sessionIds.get(sessionId);
}
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof HttpSessionDestroyedEvent) {
String sessionId = ((HttpSession) event.getSource()).getId();
removeSessionInformation(sessionId);
}
}
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
public synchronized void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (logger.isDebugEnabled()) {
logger.debug("Registering session " + sessionId +", for principal " + principal);
}
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
Set<String> sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = Collections.synchronizedSet(new HashSet<String>(4));
principals.put(principal, sessionsUsedByPrincipal);
}
sessionsUsedByPrincipal.add(sessionId);
}
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Removing session " + sessionId + " from set of registered sessions");
}
sessionIds.remove(sessionId);
Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal());
if (sessionsUsedByPrincipal == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");
}
synchronized (sessionsUsedByPrincipal) {
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.size() == 0) {
// No need to keep object in principals Map anymore
if (logger.isDebugEnabled()) {
logger.debug("Removing principal " + info.getPrincipal() + " from registry");
}
principals.remove(info.getPrincipal());
}
}
}
}
@@ -0,0 +1,6 @@
<html>
<body>
Concurrent session control and registration classes.
</body>
</html>
@@ -0,0 +1,39 @@
package org.springframework.security.context.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Used to pass the incoming request to {@link SecurityContextRepository#loadContext(HttpRequestResponseHolder)},
* allowing the method to swap the request for a wrapped version, as well as returning the <tt>SecurityContext</tt>
* value.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class HttpRequestResponseHolder {
HttpServletRequest request;
HttpServletResponse response;
public HttpRequestResponseHolder(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
HttpServletRequest getRequest() {
return request;
}
void setRequest(HttpServletRequest request) {
this.request = request;
}
HttpServletResponse getResponse() {
return response;
}
void setResponse(HttpServletResponse response) {
this.response = response;
}
}
@@ -0,0 +1,202 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.context.web;
import javax.servlet.ServletException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.context.SecurityContextImpl;
import org.springframework.security.ui.FilterChainOrder;
/**
* Populates the {@link SecurityContextHolder} with information obtained from
* the <code>HttpSession</code>.
* <p/>
* <p/>
* The <code>HttpSession</code> will be queried to retrieve the
* <code>SecurityContext</code> that should be stored against the
* <code>SecurityContextHolder</code> for the duration of the web request. At
* the end of the web request, any updates made to the
* <code>SecurityContextHolder</code> will be persisted back to the
* <code>HttpSession</code> by this filter.
* </p>
* <p/>
* If a valid <code>SecurityContext</code> cannot be obtained from the
* <code>HttpSession</code> for whatever reason, a fresh
* <code>SecurityContext</code> will be created and used instead. The created
* object will be of the instance defined by the {@link #setContextClass(Class)}
* method (which defaults to {@link org.springframework.security.context.SecurityContextImpl}.
* </p>
* <p/>
* No <code>HttpSession</code> will be created by this filter if one does not
* already exist. If at the end of the web request the <code>HttpSession</code>
* does not exist, a <code>HttpSession</code> will <b>only</b> be created if
* the current contents of the <code>SecurityContextHolder</code> are not
* {@link java.lang.Object#equals(java.lang.Object)} to a <code>new</code>
* instance of {@link #setContextClass(Class)}. This avoids needless
* <code>HttpSession</code> creation, but automates the storage of changes
* made to the <code>SecurityContextHolder</code>. There is one exception to
* this rule, that is if the {@link #forceEagerSessionCreation} property is
* <code>true</code>, in which case sessions will always be created
* irrespective of normal session-minimisation logic (the default is
* <code>false</code>, as this is resource intensive and not recommended).
* </p>
* <p/>
* This filter will only execute once per request, to resolve servlet container
* (specifically Weblogic) incompatibilities.
* </p>
* <p/>
* If for whatever reason no <code>HttpSession</code> should <b>ever</b> be
* created (eg this filter is only being used with Basic authentication or
* similar clients that will never present the same <code>jsessionid</code>
* etc), the {@link #setAllowSessionCreation(boolean)} should be set to
* <code>false</code>. Only do this if you really need to conserve server
* memory and ensure all classes using the <code>SecurityContextHolder</code>
* are designed to have no persistence of the <code>SecurityContext</code>
* between web requests. Please note that if {@link #forceEagerSessionCreation}
* is <code>true</code>, the <code>allowSessionCreation</code> must also be
* <code>true</code> (setting it to <code>false</code> will cause a startup
* time error).
* </p>
* <p/>
* This filter MUST be executed BEFORE any authentication processing mechanisms.
* Authentication processing mechanisms (eg BASIC, CAS processing filters etc)
* expect the <code>SecurityContextHolder</code> to contain a valid
* <code>SecurityContext</code> by the time they execute.
* </p>
*
* @author Ben Alex
* @author Patrick Burleson
* @author Luke Taylor
* @author Martin Algesten
*
* @deprecated Use SecurityContextPersistenceFilter instead.
*
* @version $Id$
*/
public class HttpSessionContextIntegrationFilter extends SecurityContextPersistenceFilter implements InitializingBean {
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_CONTEXT_KEY = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
//~ Instance fields ================================================================================================
private Class<? extends SecurityContext> contextClass = SecurityContextImpl.class;
/**
* Indicates if this filter can create a <code>HttpSession</code> if
* needed (sessions are always created sparingly, but setting this value to
* <code>false</code> will prohibit sessions from ever being created).
* Defaults to <code>true</code>. Do not set to <code>false</code> if
* you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
* as the properties would be in conflict.
*/
private boolean allowSessionCreation = true;
/**
* Indicates if this filter is required to create a <code>HttpSession</code>
* for every request before proceeding through the filter chain, even if the
* <code>HttpSession</code> would not ordinarily have been created. By
* default this is <code>false</code>, which is entirely appropriate for
* most circumstances as you do not want a <code>HttpSession</code>
* created unless the filter actually needs one. It is envisaged the main
* situation in which this property would be set to <code>true</code> is
* if using other filters that depend on a <code>HttpSession</code>
* already existing, such as those which need to obtain a session ID. This
* is only required in specialised cases, so leave it set to
* <code>false</code> unless you have an actual requirement and are
* conscious of the session creation overhead.
*/
private boolean forceEagerSessionCreation = false;
/**
* Indicates whether the <code>SecurityContext</code> will be cloned from
* the <code>HttpSession</code>. The default is to simply reference (ie
* the default is <code>false</code>). The default may cause issues if
* concurrent threads need to have a different security identity from other
* threads being concurrently processed that share the same
* <code>HttpSession</code>. In most normal environments this does not
* represent an issue, as changes to the security identity in one thread is
* allowed to affect the security identitiy in other threads associated with
* the same <code>HttpSession</code>. For unusual cases where this is not
* permitted, change this value to <code>true</code> and ensure the
* {@link #contextClass} is set to a <code>SecurityContext</code> that
* implements {@link Cloneable} and overrides the <code>clone()</code>
* method.
*/
private boolean cloneFromHttpSession = false;
// private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
public HttpSessionContextIntegrationFilter() throws ServletException {
super.setSecurityContextRepository(repo);
}
public boolean isCloneFromHttpSession() {
return cloneFromHttpSession;
}
public void setCloneFromHttpSession(boolean cloneFromHttpSession) {
this.cloneFromHttpSession = cloneFromHttpSession;
repo.setCloneFromHttpSession(cloneFromHttpSession);
}
public boolean isAllowSessionCreation() {
return allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
repo.setAllowSessionCreation(allowSessionCreation);
}
protected Class<? extends SecurityContext> getContextClass() {
return contextClass;
}
@SuppressWarnings("unchecked")
public void setContextClass(Class secureContext) {
this.contextClass = secureContext;
repo.setSecurityContextClass(secureContext);
}
public boolean isForceEagerSessionCreation() {
return forceEagerSessionCreation;
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
super.setForceEagerSessionCreation(forceEagerSessionCreation);
}
public int getOrder() {
return FilterChainOrder.HTTP_SESSION_CONTEXT_FILTER;
}
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
if (forceEagerSessionCreation && !allowSessionCreation) {
throw new IllegalArgumentException(
"If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
}
}
}
@@ -0,0 +1,373 @@
package org.springframework.security.context.web;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.context.SecurityContextHolderStrategy;
import org.springframework.security.context.SecurityContextImpl;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* A <tt>SecurityContextRepository</tt> implementation which stores the security context in the HttpSession between
* requests.
* <p>
* The <code>HttpSession</code> will be queried to retrieve the <code>SecurityContext</code> in the <tt>loadContext</tt>
* method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY}). If a valid <code>SecurityContext</code> cannot be
* obtained from the <code>HttpSession</code> for whatever reason, a fresh <code>SecurityContext</code> will be created
* and returned instead. The created object will be an instance of the class set using the
* {@link #setSecurityContextClass(Class)} method. If this hasn't been set, a {@link SecurityContextImpl} will be returned.
* <p>
* When <tt>saveContext</tt> is called, the context will be stored under the same key, provided
* <ol>
* <li>The value has changed</li>
* <li>The configured <tt>AuthenticationTrustResolver</tt> does not report that the contents represent an anonymous
* user</li>
* </ol>
* <p>
* With the standard configuration, no <code>HttpSession</code> will be created during <tt>loadContext</tt> if one does
* not already exist. When <tt>saveContext</tt> is called at the end of the web request, and no session exists, a new
* <code>HttpSession</code> will <b>only</b> be created if the supplied <tt>SecurityContext</tt> is not equal
* to a <code>new</code> instance of the {@link #setContextClass(Class) contextClass} (or an empty
* <tt>SecurityContextImpl</tt> if the class has not been set. This avoids needless <code>HttpSession</code> creation,
* but automates the storage of changes made to the context during the request. Note that if
* {@link SecurityContextPersistenceFilter} is configured to eagerly create sessions, then the session-minimisation
* logic applied here will not make any difference. If you are using eager session creation, then you should
* ensure that the <tt>allowSessionCreation</tt> property of this class is set to <tt>true</tt> (the default).
* <p>
* If for whatever reason no <code>HttpSession</code> should <b>ever</b> be created (e.g. Basic authentication is being
* used or similar clients that will never present the same <code>jsessionid</code> etc), then
* {@link #setAllowSessionCreation(boolean) allowSessionCreation} should be set to <code>false</code>.
* Only do this if you really need to conserve server memory and ensure all classes using the
* <code>SecurityContextHolder</code> are designed to have no persistence of the <code>SecurityContext</code>
* between web requests.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
protected final Log logger = LogFactory.getLog(this.getClass());
private Class<? extends SecurityContext> securityContextClass = null;
/** SecurityContext instance used to check for equality with default (unauthenticated) content */
private Object contextObject = new SecurityContextImpl();
private boolean cloneFromHttpSession = false;
private boolean allowSessionCreation = true;
private boolean disableUrlRewriting = false;
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
/**
* Gets the security context from the session (if available) and returns it.
* <p>
* If the session is null, the context object is null or the context object stored in the session
* is not an instance of <tt>SecurityContext</tt>, a new context object will be generated and
* returned.
* <p>
* If <tt>cloneFromHttpSession</tt> is set to true, it will attempt to clone the context object first
* and return the cloned instance.
*/
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
if (logger.isDebugEnabled()) {
logger.debug("No SecurityContext was available from the HttpSession: " + httpSession +". " +
"A new one will be created.");
}
context = generateNewContext();
}
requestResponseHolder.setResponse(new SaveToSessionResponseWrapper(response, request,
httpSession != null, context.hashCode()));
return context;
}
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
SaveToSessionResponseWrapper responseWrapper = (SaveToSessionResponseWrapper)response;
// saveContext() might already be called by the response wrapper
// if something in the chain called sendError() or sendRedirect(). This ensures we only call it
// once per request.
if (!responseWrapper.isContextSaved() ) {
responseWrapper.saveContext(context);
}
}
/**
*
* @param httpSession the session obtained from the request.
*/
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
if (logger.isDebugEnabled()) {
logger.debug("No HttpSession currently exists");
}
return null;
}
// Session exists, so try to obtain a context from it.
Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
if (contextFromSession == null) {
if (logger.isDebugEnabled()) {
logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
}
return null;
}
// We now have the security context object from the session.
if (!(contextFromSession instanceof SecurityContext)) {
if (logger.isWarnEnabled()) {
logger.warn("SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
+ contextFromSession + "'; are you improperly modifying the HttpSession directly "
+ "(you should always use SecurityContextHolder) or using the HttpSession attribute "
+ "reserved for this class?");
}
return null;
}
// Clone if required (see SEC-356)
if (cloneFromHttpSession) {
contextFromSession = cloneContext(contextFromSession);
}
if (logger.isDebugEnabled()) {
logger.debug("Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: '" + contextFromSession + "'");
}
// Everything OK. The only non-null return from this method.
return (SecurityContext) contextFromSession;
}
/**
*
* @param context the object which was stored under the security context key in the HttpSession.
* @return the cloned SecurityContext object. Never null.
*/
private Object cloneContext(Object context) {
Object clonedContext = null;
Assert.isInstanceOf(Cloneable.class, context,
"Context must implement Cloneable and provide a Object.clone() method");
try {
Method m = context.getClass().getMethod("clone", new Class[]{});
if (!m.isAccessible()) {
m.setAccessible(true);
}
clonedContext = m.invoke(context, new Object[]{});
} catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
return clonedContext;
}
/**
* By default, calls {@link SecurityContextHolder#createEmptyContext()} to obtain a new context (there should be
* no context present in the holder when this method is called). Using this approach the context creation
* strategy is decided by the {@link SecurityContextHolderStrategy} in use. The default implementations
* will return a new <tt>SecurityContextImpl</tt>.
* <p>
* An alternative way of customizing the <tt>SecurityContext</tt> implementation is by setting the
* <tt>securityContextClass</tt> property. In this case, the method will attempt to invoke the no-args
* constructor on the supplied class instead and return the created instance.
*
* @return a new SecurityContext instance. Never null.
*/
SecurityContext generateNewContext() {
SecurityContext context = null;
if (securityContextClass == null) {
context = SecurityContextHolder.createEmptyContext();
return context;
}
try {
context = securityContextClass.newInstance();
} catch (Exception e) {
ReflectionUtils.handleReflectionException(e);
}
return context;
}
@SuppressWarnings("unchecked")
public void setSecurityContextClass(Class contextClass) {
if (contextClass == null || (!SecurityContext.class.isAssignableFrom(contextClass))) {
throw new IllegalArgumentException("securityContextClass must implement SecurityContext "
+ "(typically use org.springframework.security.context.SecurityContextImpl; existing class is "
+ contextClass + ")");
}
this.securityContextClass = contextClass;
contextObject = generateNewContext();
}
public void setCloneFromHttpSession(boolean cloneFromHttpSession) {
this.cloneFromHttpSession = cloneFromHttpSession;
}
/**
* If set to true (the default), a new session will be created if to store the security context if it is determined
* that it's contents are different from the default.
*
* @param allowSessionCreation
*/
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
/**
* Allows the use of session identifiers in URLs to be disabled. Off by default.
*
* @param disableUrlRewriting set to <tt>true</tt> to disable URL encoding methods in the response wrapper
* and prevent the use of <tt>jsessionid</tt> parameters.
*/
public void setDisableUrlRewriting(boolean disableUrlRewriting) {
this.disableUrlRewriting = disableUrlRewriting;
}
//~ Inner Classes ==================================================================================================
/**
* Wrapper that is applied to every request/response to update the <code>HttpSession<code> with
* the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
* happens. See SEC-398.
* <p>
* Stores the necessary state from the start of the request in order to make a decision about whether
* the security context has changed before saving it.
*/
class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper {
private HttpServletRequest request;
private boolean httpSessionExistedAtStartOfRequest;
private int contextHashBeforeChainExecution;
/**
* Takes the parameters required to call <code>saveContext()</code> successfully in
* addition to the request and the response object we are wrapping.
*
* @param request the request object (used to obtain the session, if one exists).
* @param httpSessionExistedAtStartOfRequest indicates whether there was a session in place before the
* filter chain executed. If this is true, and the session is found to be null, this indicates that it was
* invalidated during the request and a new session will now be created.
* @param contextHashBeforeChainExecution the hashcode of the context before the filter chain executed.
* The context will only be stored if it has a different hashcode, indicating that the context changed
* during the request.
*/
SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request,
boolean httpSessionExistedAtStartOfRequest,
int contextHashBeforeChainExecution) {
super(response, disableUrlRewriting);
this.request = request;
this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
this.contextHashBeforeChainExecution = contextHashBeforeChainExecution;
}
/**
* Stores the supplied security context in the session (if available) and if it has changed since it was
* set at the start of the request. If the AuthenticationTrustResolver identifies the current user as
* anonymous, then the context will not be stored.
*
* @param context the context object obtained from the SecurityContextHolder after the request has
* been processed by the filter chain. SecurityContextHolder.getContext() cannot be used to obtain
* the context as it has already been cleared by the time this method is called.
*
*/
@Override
void saveContext(SecurityContext context) {
HttpSession httpSession = request.getSession(false);
if (httpSession == null) {
httpSession = createNewSessionIfAllowed(context);
}
// If HttpSession exists, store current SecurityContextHolder contents but only if
// the SecurityContext has actually changed (see JIRA SEC-37)
if (httpSession != null && context.hashCode() != contextHashBeforeChainExecution) {
// See SEC-776
// TODO: Move this so that a session isn't created if user is anonymous
if (authenticationTrustResolver.isAnonymous(context.getAuthentication())) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext contents are anonymous - context will not be stored in HttpSession. ");
}
} else {
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext stored to HttpSession: '" + context + "'");
}
}
}
}
private HttpSession createNewSessionIfAllowed(SecurityContext context) {
if (httpSessionExistedAtStartOfRequest) {
if (logger.isDebugEnabled()) {
logger.debug("HttpSession is now null, but was not null at start of request; "
+ "session was invalidated, so do not create a new session");
}
return null;
}
if (!allowSessionCreation) {
if (logger.isDebugEnabled()) {
logger.debug("The HttpSession is currently null, and the "
+ "HttpSessionContextIntegrationFilter is prohibited from creating an HttpSession "
+ "(because the allowSessionCreation property is false) - SecurityContext thus not "
+ "stored for next request");
}
return null;
}
// Generate a HttpSession only if we need to
if (contextObject.equals(context)) {
if (logger.isDebugEnabled()) {
logger.debug("HttpSession is null, but SecurityContext has not changed from default: ' "
+ context
+ "'; not creating HttpSession or storing SecurityContext");
}
return null;
}
if (logger.isDebugEnabled()) {
logger.debug("HttpSession being created as SecurityContext is non-default");
}
try {
return request.getSession(true);
} catch (IllegalStateException e) {
// Response must already be committed, therefore can't create a new session
logger.warn("Failed to create a session, as response has been committed. Unable to store" +
" SecurityContext.");
}
return null;
}
}
}
@@ -0,0 +1,125 @@
package org.springframework.security.context.web;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;
/**
* Base class for response wrappers which encapsulate the logic for storing a security context and which
* store the with the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
* happens. See issue SEC-398.
* <p>
* Sub-classes should implement the {@link #saveContext(SecurityContext context)} method.
* <p>
* Support is also provided for disabling URL rewriting
*
* @author Luke Taylor
* @author Marten Algesten
* @version $Id$
* @since 2.5
*/
public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServletResponseWrapper {
private boolean contextSaved = false;
/* See SEC-1052 */
private boolean disableUrlRewriting;
/**
* @param response the response to be wrapped
* @param disableUrlRewriting turns the URL encoding methods into null operations, preventing the use
* of URL rewriting to add the session identifier as a URL parameter.
*/
public SaveContextOnUpdateOrErrorResponseWrapper(HttpServletResponse response, boolean disableUrlRewriting) {
super(response);
this.disableUrlRewriting = disableUrlRewriting;
}
/**
* Implements the logic for storing the security context.
*
* @param context the <tt>SecurityContext</tt> instance to store
*/
abstract void saveContext(SecurityContext context);
/**
* Makes sure the session is updated before calling the
* superclass <code>sendError()</code>
*/
@Override
public final void sendError(int sc) throws IOException {
doSaveContext();
super.sendError(sc);
}
/**
* Makes sure the session is updated before calling the
* superclass <code>sendError()</code>
*/
@Override
public final void sendError(int sc, String msg) throws IOException {
doSaveContext();
super.sendError(sc, msg);
}
/**
* Makes sure the context is stored before calling the
* superclass <code>sendRedirect()</code>
*/
@Override
public final void sendRedirect(String location) throws IOException {
doSaveContext();
super.sendRedirect(location);
}
/**
* Calls <code>saveContext()</code> with the current contents of the <tt>SecurityContextHolder</tt>.
*/
private void doSaveContext() {
saveContext(SecurityContextHolder.getContext());
contextSaved = true;
}
@Override
public final String encodeRedirectUrl(String url) {
if (disableUrlRewriting) {
return url;
}
return super.encodeRedirectUrl(url);
}
@Override
public final String encodeRedirectURL(String url) {
if (disableUrlRewriting) {
return url;
}
return super.encodeRedirectURL(url);
}
@Override
public final String encodeUrl(String url) {
if (disableUrlRewriting) {
return url;
}
return super.encodeUrl(url);
}
@Override
public final String encodeURL(String url) {
if (disableUrlRewriting) {
return url;
}
return super.encodeURL(url);
}
/**
* Tells if the response wrapper has called <code>saveContext()</code> because of an error or redirect.
*/
public final boolean isContextSaved() {
return contextSaved;
}
}
@@ -0,0 +1,97 @@
package org.springframework.security.context.web;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
/**
* Populates the {@link SecurityContextHolder} with information obtained from
* the configured {@link SecurityContextRepository} prior to the request and stores it back in the repository
* once the request has completed. By default it uses an {@link HttpSessionSecurityContextRepository}. See this
* class for information <tt>HttpSession</tt> related configuration options.
* <p>
* This filter will only execute once per request, to resolve servlet container (specifically Weblogic)
* incompatibilities.
* <p>
* This filter MUST be executed BEFORE any authentication processing mechanisms. Authentication processing mechanisms
* (e.g. BASIC, CAS processing filters etc) expect the <code>SecurityContextHolder</code> to contain a valid
* <code>SecurityContext</code> by the time they execute.
* <p>
* This is essentially a refactoring of the old <tt>HttpSessionContextIntegrationFilter</tt> to delegate
* the storage issues to a separate strategy, allowing for more customization in the way the security context is
* maintained between requests.
* <p>
* The <tt>forceEagerSessionCreation</tt> property can be used to ensure that a session is always available before
* the filter chain executes (the default is <code>false</code>, as this is resource intensive and not recommended).
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class SecurityContextPersistenceFilter extends SpringSecurityFilter {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
private SecurityContextRepository repo = new HttpSessionSecurityContextRepository();
private boolean forceEagerSessionCreation = false;
@Override
protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
logger.debug("Eagerly created session: " + session.getId());
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
} finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
public void setSecurityContextRepository(SecurityContextRepository repo) {
this.repo = repo;
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
}
public int getOrder() {
return FilterChainOrder.SECURITY_CONTEXT_FILTER;
}
}
@@ -0,0 +1,53 @@
package org.springframework.security.context.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.context.SecurityContext;
/**
* Strategy used for persisting a {@link SecurityContext} between requests.
* <p>
* Used by {@link SecurityContextPersistenceFilter} to obtain the context which should be used for the current thread
* of execution and to store the context once it has been removed from thread-local storage and the request has
* completed.
* <p>
* The persistence mechanism used will depend on the implementation, but most commonly the <tt>HttpSession</tt> will
* be used to store the context.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*
* @see SecurityContextPersistenceFilter
* @see HttpSessionSecurityContextRepository
* @see SaveContextOnUpdateOrErrorResponseWrapper
*/
public interface SecurityContextRepository {
/**
* Obtains the security context for the supplied request. For an unauthenticated user, an empty context
* implementation should be returned. This method should not return null.
* <p>
* The use of the <tt>HttpRequestResponseHolder</tt> parameter allows implementations to return wrapped versions of
* the request or response (or both), allowing them to access implementation-specific state for the request.
* The values obtained from the holder will be passed on to the filter chain and also to the <tt>saveContext</tt>
* method when it is finally called. Implementations may wish to return a subclass of
* {@link SaveContextOnUpdateOrErrorResponseWrapper} as the response object, which guarantees that the context is
* persisted when an error or redirect occurs.
*
* @param requestResponseHolder holder for the current request and response for which the context should be loaded.
*
* @return The security context which should be used for the current request, never null.
*/
SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
/**
* Stores the security context on completion of a request.
*
* @param context the non-null context which was obtained from the holder.
* @param request
* @param response
*/
void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
}
@@ -0,0 +1,59 @@
package org.springframework.security.expression.web;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.intercept.web.RequestKey;
import org.springframework.security.util.UrlMatcher;
import org.springframework.util.Assert;
/**
* Expression-based <tt>FilterInvocationSecurityMetadataSource</tt>.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public final class ExpressionBasedFilterInvocationSecurityMetadataSource extends DefaultFilterInvocationSecurityMetadataSource {
private final static Log logger = LogFactory.getLog(ExpressionBasedFilterInvocationSecurityMetadataSource.class);
public ExpressionBasedFilterInvocationSecurityMetadataSource(UrlMatcher urlMatcher,
LinkedHashMap<RequestKey, List<ConfigAttribute>> requestMap, WebSecurityExpressionHandler expressionHandler) {
super(urlMatcher, processMap(requestMap, expressionHandler.getExpressionParser()));
Assert.notNull(expressionHandler, "A non-null SecurityExpressionHandler is required");
}
private static LinkedHashMap<RequestKey, List<ConfigAttribute>> processMap(
LinkedHashMap<RequestKey, List<ConfigAttribute>> requestMap, ExpressionParser parser) {
Assert.notNull(parser, "SecurityExpressionHandler returned a null parser object");
LinkedHashMap<RequestKey, List<ConfigAttribute>> requestToExpressionAttributesMap =
new LinkedHashMap<RequestKey, List<ConfigAttribute>>(requestMap);
for (Map.Entry<RequestKey, List<ConfigAttribute>> entry : requestMap.entrySet()) {
RequestKey request = entry.getKey();
Assert.isTrue(entry.getValue().size() == 1, "Expected a single expression attribute for " + request);
ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(1);
String expression = entry.getValue().get(0).getAttribute();
logger.debug("Adding web access control expression '" + expression + "', for " + request);
try {
attributes.add(new WebExpressionConfigAttribute(parser.parseExpression(expression)));
} catch (ParseException e) {
throw new IllegalArgumentException("Failed to parse expression '" + expression + "'");
}
requestToExpressionAttributesMap.put(request, attributes);
}
return requestToExpressionAttributesMap;
}
}
@@ -0,0 +1,32 @@
package org.springframework.security.expression.web;
import org.springframework.expression.Expression;
import org.springframework.security.ConfigAttribute;
/**
* Simple expression configuration attribute for use in web request authorizations.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
class WebExpressionConfigAttribute implements ConfigAttribute {
private final Expression authorizeExpression;
public WebExpressionConfigAttribute(Expression authorizeExpression) {
this.authorizeExpression = authorizeExpression;
}
Expression getAuthorizeExpression() {
return authorizeExpression;
}
public String getAttribute() {
return null;
}
@Override
public String toString() {
return authorizeExpression.getExpressionString();
}
}
@@ -0,0 +1,62 @@
package org.springframework.security.expression.web;
import java.util.List;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.Authentication;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.expression.ExpressionUtils;
import org.springframework.security.expression.MethodSecurityExpressionHandler;
import org.springframework.security.expression.support.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.expression.web.support.DefaultWebSecurityExpressionHandler;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.security.vote.AccessDecisionVoter;
/**
* Voter which handles web authorisation decisions.
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class WebExpressionVoter implements AccessDecisionVoter {
private WebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
public int vote(Authentication authentication, Object object, List<ConfigAttribute> attributes) {
assert authentication != null;
assert object != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
FilterInvocation fi = (FilterInvocation)object;
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ?
ACCESS_GRANTED : ACCESS_DENIED;
}
private WebExpressionConfigAttribute findConfigAttribute(List<ConfigAttribute> attributes) {
for (ConfigAttribute attribute : attributes) {
if (attribute instanceof WebExpressionConfigAttribute) {
return (WebExpressionConfigAttribute)attribute;
}
}
return null;
}
public boolean supports(ConfigAttribute attribute) {
return attribute instanceof WebExpressionConfigAttribute;
}
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(FilterInvocation.class);
}
public void setExpressionHandler(WebSecurityExpressionHandler expressionHandler) {
this.expressionHandler = expressionHandler;
}
}
@@ -0,0 +1,19 @@
package org.springframework.security.expression.web;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.security.Authentication;
import org.springframework.security.intercept.web.FilterInvocation;
public interface WebSecurityExpressionHandler {
/**
* @return an expression parser for the expressions used by the implementation.
*/
ExpressionParser getExpressionParser();
/**
* Provides an evaluation context in which to evaluate security expressions for a web invocation.
*/
EvaluationContext createEvaluationContext(Authentication authentication, FilterInvocation fi);
}
@@ -0,0 +1,39 @@
package org.springframework.security.expression.web.support;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.antlr.SpelAntlrExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.expression.support.SecurityExpressionRoot;
import org.springframework.security.expression.web.WebSecurityExpressionHandler;
import org.springframework.security.intercept.web.FilterInvocation;
/**
* Facade which isolates Spring Security's requirements for evaluating web-security expressions
* from the implementation of the underlying expression objects.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class DefaultWebSecurityExpressionHandler implements WebSecurityExpressionHandler {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private ExpressionParser expressionParser = new SpelAntlrExpressionParser();
public ExpressionParser getExpressionParser() {
return expressionParser;
}
public EvaluationContext createEvaluationContext(Authentication authentication, FilterInvocation fi) {
StandardEvaluationContext ctx = new StandardEvaluationContext();
SecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, fi);
root.setTrustResolver(trustResolver);
ctx.setRootObject(root);
return ctx;
}
}
@@ -0,0 +1,86 @@
package org.springframework.security.expression.web.support;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import org.springframework.security.Authentication;
import org.springframework.security.expression.support.SecurityExpressionRoot;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.util.StringUtils;
/**
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
class WebSecurityExpressionRoot extends SecurityExpressionRoot {
private FilterInvocation filterInvocation;
WebSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
super(a);
this.filterInvocation = fi;
}
/**
* Takes a specific IP address or a range using the IP/Netmask (e.g. 192.168.1.0/24 or 202.24.0.0/14).
*
* @param ipAddress the address or range of addresses from which the request must come.
* @return true if the IP address of the current request is in the required range.
*/
public boolean hasIpAddress(String ipAddress) {
int nMaskBits = 0;
if (ipAddress.indexOf('/') > 0) {
String[] addressAndMask = StringUtils.split(ipAddress, "/");
ipAddress = addressAndMask[0];
nMaskBits = Integer.parseInt(addressAndMask[1]);
}
InetAddress requiredAddress = parseAddress(ipAddress);
InetAddress remoteAddress = parseAddress(filterInvocation.getHttpRequest().getRemoteAddr());
if (!requiredAddress.getClass().equals(remoteAddress.getClass())) {
throw new IllegalArgumentException("IP Address in expression must be the same type as " +
"version returned by request");
}
if (nMaskBits == 0) {
return remoteAddress.equals(requiredAddress);
}
byte[] remAddr = remoteAddress.getAddress();
byte[] reqAddr = requiredAddress.getAddress();
int oddBits = nMaskBits % 8;
int nMaskBytes = nMaskBits/8 + (oddBits == 0 ? 0 : 1);
byte[] mask = new byte[nMaskBytes];
Arrays.fill(mask, 0, oddBits == 0 ? mask.length : mask.length - 1, (byte)0xFF);
if (oddBits != 0) {
int finalByte = (1 << oddBits) - 1;
finalByte <<= 8-oddBits;
mask[mask.length - 1] = (byte) finalByte;
}
// System.out.println("Mask is " + new sun.misc.HexDumpEncoder().encode(mask));
for (int i=0; i < mask.length; i++) {
if ((remAddr[i] & mask[i]) != (reqAddr[i] & mask[i])) {
return false;
}
}
return true;
}
private InetAddress parseAddress(String address) {
try {
return InetAddress.getByName(address);
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Failed to parse address" + address, e);
}
}
}
@@ -0,0 +1,224 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.intercept.web;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.util.UrlMatcher;
/**
* Default implementation of <tt>FilterInvocationDefinitionSource</tt>.
* <p>
* Stores an ordered map of compiled URL paths to <tt>ConfigAttribute</tt> lists and provides URL matching
* against the items stored in this map using the configured <tt>UrlMatcher</tt>.
* <p>
* The order of registering the regular expressions using the
* {@link #addSecureUrl(String, List<ConfigAttribute>)} is very important.
* The system will identify the <b>first</b> matching regular
* expression for a given HTTP URL. It will not proceed to evaluate later regular expressions if a match has already
* been found. Accordingly, the most specific regular expressions should be registered first, with the most general
* regular expressions registered last.
* <p>
* If URLs are registered for a particular HTTP method using
* {@link #addSecureUrl(String, String, List<ConfigAttribute>)}, then the method-specific matches will take
* precedence over any URLs which are registered without an HTTP method.
*
* @author Ben Alex
* @author Luke Taylor
* @version $Id$
*/
public class DefaultFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static final Set<String> HTTP_METHODS = new HashSet<String>(Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"));
protected final Log logger = LogFactory.getLog(getClass());
//private Map<Object, List<ConfigAttribute>> requestMap = new LinkedHashMap<Object, List<ConfigAttribute>>();
/** Stores request maps keyed by specific HTTP methods. A null key matches any method */
private Map<String, Map<Object, List<ConfigAttribute>>> httpMethodMap =
new HashMap<String, Map<Object, List<ConfigAttribute>>>();
private UrlMatcher urlMatcher;
private boolean stripQueryStringFromUrls;
//~ Constructors ===================================================================================================
/**
* Builds the internal request map from the supplied map. The key elements should be of type {@link RequestKey},
* which contains a URL path and an optional HTTP method (may be null). The path stored in the key will depend on
* the type of the supplied UrlMatcher.
*
* @param urlMatcher typically an ant or regular expression matcher.
* @param requestMap order-preserving map of request definitions to attribute lists
*/
public DefaultFilterInvocationSecurityMetadataSource(UrlMatcher urlMatcher,
LinkedHashMap<RequestKey, List<ConfigAttribute>> requestMap) {
this.urlMatcher = urlMatcher;
for (Map.Entry<RequestKey, List<ConfigAttribute>> entry : requestMap.entrySet()) {
addSecureUrl(entry.getKey().getUrl(), entry.getKey().getMethod(), entry.getValue());
}
}
//~ Methods ========================================================================================================
/**
* Adds a URL,attribute-list pair to the request map, first allowing the <tt>UrlMatcher</tt> to
* process the pattern if required, using its <tt>compile</tt> method. The returned object will be used as the key
* to the request map and will be passed back to the <tt>UrlMatcher</tt> when iterating through the map to find
* a match for a particular URL.
*/
private void addSecureUrl(String pattern, String method, List<ConfigAttribute> attr) {
Map<Object, List<ConfigAttribute>> mapToUse = getRequestMapForHttpMethod(method);
mapToUse.put(urlMatcher.compile(pattern), attr);
if (logger.isDebugEnabled()) {
logger.debug("Added URL pattern: " + pattern + "; attributes: " + attr +
(method == null ? "" : " for HTTP method '" + method + "'"));
}
}
/**
* Return the HTTP method specific request map, creating it if it doesn't already exist.
* @param method GET, POST etc
* @return map of URL patterns to <tt>ConfigAttribute</tt>s for this method.
*/
private Map<Object, List<ConfigAttribute>> getRequestMapForHttpMethod(String method) {
if (method != null && !HTTP_METHODS.contains(method)) {
throw new IllegalArgumentException("Unrecognised HTTP method: '" + method + "'");
}
Map<Object, List<ConfigAttribute>> methodRequestMap = httpMethodMap.get(method);
if (methodRequestMap == null) {
methodRequestMap = new LinkedHashMap<Object, List<ConfigAttribute>>();
httpMethodMap.put(method, methodRequestMap);
}
return methodRequestMap;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
for (Map.Entry<String, Map<Object, List<ConfigAttribute>>> entry : httpMethodMap.entrySet()) {
for (List<ConfigAttribute> attrs : entry.getValue().values()) {
allAttributes.addAll(attrs);
}
}
return allAttributes;
}
public List<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if ((object == null) || !this.supports(object.getClass())) {
throw new IllegalArgumentException("Object must be a FilterInvocation");
}
String url = ((FilterInvocation) object).getRequestUrl();
String method = ((FilterInvocation) object).getHttpRequest().getMethod();
return lookupAttributes(url, method);
}
/**
* Performs the actual lookup of the relevant <tt>ConfigAttribute</tt>s for the given <code>FilterInvocation</code>.
* <p>
* By default, iterates through the stored URL map and calls the
* {@link UrlMatcher#pathMatchesUrl(Object path, String url)} method until a match is found.
* <p>
* Subclasses can override if required to perform any modifications to the URL.
*
* @param url the URI to retrieve configuration attributes for
* @param method the HTTP method (GET, POST, DELETE...).
*
* @return the <code>ConfigAttribute</code>s that apply to the specified <code>FilterInvocation</code>
* or null if no match is found
*/
public final List<ConfigAttribute> lookupAttributes(String url, String method) {
if (stripQueryStringFromUrls) {
// Strip anything after a question mark symbol, as per SEC-161. See also SEC-321
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
}
if (urlMatcher.requiresLowerCaseUrl()) {
url = url.toLowerCase();
if (logger.isDebugEnabled()) {
logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
}
}
// Obtain the map of request patterns to attributes for this method and lookup the url.
Map<Object, List<ConfigAttribute>> requestMap = httpMethodMap.get(method);
// If no method-specific map, use the general one stored under the null key
if (requestMap == null) {
requestMap = httpMethodMap.get(null);
}
if (requestMap != null) {
for (Map.Entry<Object, List<ConfigAttribute>> entry : requestMap.entrySet()) {
Object p = entry.getKey();
boolean matched = urlMatcher.pathMatchesUrl(entry.getKey(), url);
if (logger.isDebugEnabled()) {
logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched);
}
if (matched) {
return entry.getValue();
}
}
}
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
protected UrlMatcher getUrlMatcher() {
return urlMatcher;
}
public boolean isConvertUrlToLowercaseBeforeComparison() {
return urlMatcher.requiresLowerCaseUrl();
}
public void setStripQueryStringFromUrls(boolean stripQueryStringFromUrls) {
this.stripQueryStringFromUrls = stripQueryStringFromUrls;
}
}
@@ -0,0 +1,104 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.intercept.web;
import org.springframework.security.web.util.UrlUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Holds objects associated with a HTTP filter.<P>Guarantees the request and response are instances of
* <code>HttpServletRequest</code> and <code>HttpServletResponse</code>, and that there are no <code>null</code>
* objects.
* <p>
* Required so that security system classes can obtain access to the filter environment, as well as the request
* and response.
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class FilterInvocation {
//~ Instance fields ================================================================================================
private FilterChain chain;
private HttpServletRequest request;
private HttpServletResponse response;
//~ Constructors ===================================================================================================
public FilterInvocation(ServletRequest request, ServletResponse response, FilterChain chain) {
if ((request == null) || (response == null) || (chain == null)) {
throw new IllegalArgumentException("Cannot pass null values to constructor");
}
this.request = (HttpServletRequest) request;
this.response = (HttpServletResponse) response;
this.chain = chain;
}
//~ Methods ========================================================================================================
public FilterChain getChain() {
return chain;
}
/**
* Indicates the URL that the user agent used for this request.
* <p>
* The returned URL does <b>not</b> reflect the port number determined from a
* {@link org.springframework.security.web.util.PortResolver}.
*
* @return the full URL of this request
*/
public String getFullRequestUrl() {
return UrlUtils.getFullRequestUrl(this);
}
public HttpServletRequest getHttpRequest() {
return request;
}
public HttpServletResponse getHttpResponse() {
return response;
}
/**
* Obtains the web application-specific fragment of the URL.
*
* @return the URL, excluding any server name, context path or servlet path
*/
public String getRequestUrl() {
return UrlUtils.getRequestUrl(this);
}
public HttpServletRequest getRequest() {
return getHttpRequest();
}
public HttpServletResponse getResponse() {
return getHttpResponse();
}
public String toString() {
return "FilterInvocation: URL: " + getRequestUrl();
}
}
@@ -0,0 +1,28 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.intercept.web;
import org.springframework.security.intercept.SecurityMetadataSource;
/**
* Marker interface for <code>SecurityMetadataSource</code> implementations
* that are designed to perform lookups keyed on {@link FilterInvocation}s.
*
* @author Ben Alex
* @version $Id$
*/
public interface FilterInvocationSecurityMetadataSource extends SecurityMetadataSource {}
@@ -0,0 +1,155 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.intercept.web;
import org.springframework.security.intercept.AbstractSecurityInterceptor;
import org.springframework.security.intercept.InterceptorStatusToken;
import org.springframework.security.intercept.SecurityMetadataSource;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.core.Ordered;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* Performs security handling of HTTP resources via a filter implementation.
* <p>
* The <code>SecurityMetadataSource</code> required by this security interceptor is of type {@link
* FilterInvocationSecurityMetadataSource}.
* <p>
* Refer to {@link AbstractSecurityInterceptor} for details on the workflow.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter, Ordered {
//~ Static fields/initializers =====================================================================================
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
//~ Instance fields ================================================================================================
private FilterInvocationSecurityMetadataSource securityMetadataSource;
private boolean observeOncePerRequest = true;
//~ Methods ========================================================================================================
/**
* Not used (we rely on IoC container lifecycle services instead)
*
* @param arg0 ignored
*
* @throws ServletException never thrown
*/
public void init(FilterConfig arg0) throws ServletException {}
/**
* Not used (we rely on IoC container lifecycle services instead)
*/
public void destroy() {}
/**
* Method that is actually called by the filter chain. Simply delegates to the {@link
* #invoke(FilterInvocation)} method.
*
* @param request the servlet request
* @param response the servlet response
* @param chain the filter chain
*
* @throws IOException if the filter chain fails
* @throws ServletException if the filter chain fails
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return this.securityMetadataSource;
}
public Class<? extends Object> getSecureObjectClass() {
return FilterInvocation.class;
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}
/**
* Indicates whether once-per-request handling will be observed. By default this is <code>true</code>,
* meaning the <code>FilterSecurityInterceptor</code> will only execute once-per-request. Sometimes users may wish
* it to execute more than once per request, such as when JSP forwards are being used and filter security is
* desired on each included fragment of the HTTP request.
*
* @return <code>true</code> (the default) if once-per-request is honoured, otherwise <code>false</code> if
* <code>FilterSecurityInterceptor</code> will enforce authorizations for each and every fragment of the
* HTTP request.
*/
public boolean isObserveOncePerRequest() {
return observeOncePerRequest;
}
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
/**
* @deprecated use setSecurityMetadataSource instead
*/
public void setObjectDefinitionSource(FilterInvocationSecurityMetadataSource newSource) {
logger.warn("The property 'objectDefinitionSource' is deprecated. Please use 'securityMetadataSource' instead");
this.securityMetadataSource = newSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
public void setObserveOncePerRequest(boolean observeOncePerRequest) {
this.observeOncePerRequest = observeOncePerRequest;
}
public int getOrder() {
return FilterChainOrder.FILTER_SECURITY_INTERCEPTOR;
}
}
@@ -0,0 +1,69 @@
package org.springframework.security.intercept.web;
/**
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public class RequestKey {
private String url;
private String method;
public RequestKey(String url) {
this(url, null);
}
public RequestKey(String url, String method) {
this.url = url;
this.method = method;
}
String getUrl() {
return url;
}
String getMethod() {
return method;
}
public int hashCode() {
int code = 31;
code ^= url.hashCode();
if (method != null) {
code ^= method.hashCode();
}
return code;
}
public boolean equals(Object obj) {
if (!(obj instanceof RequestKey)) {
return false;
}
RequestKey key = (RequestKey) obj;
if (!url.equals(key.url)) {
return false;
}
if (method == null) {
return key.method == null;
}
return method.equals(key.method);
}
public String toString() {
StringBuffer sb = new StringBuffer(url.length() + 7);
sb.append("[");
if (method != null) {
sb.append(method).append(",");
}
sb.append(url);
sb.append("]");
return sb.toString();
}
}
@@ -0,0 +1,90 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.intercept.web;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.Authentication;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.AbstractSecurityInterceptor;
import org.springframework.util.Assert;
/**
* Allows users to determine whether they have privileges for a given web URI.
*
* @author Ben Alex
* @version $Id$
*/
public class WebInvocationPrivilegeEvaluator implements InitializingBean {
//~ Static fields/initializers =====================================================================================
protected static final Log logger = LogFactory.getLog(WebInvocationPrivilegeEvaluator.class);
//~ Instance fields ================================================================================================
private AbstractSecurityInterceptor securityInterceptor;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(securityInterceptor, "SecurityInterceptor required");
}
public boolean isAllowed(FilterInvocation fi, Authentication authentication) {
Assert.notNull(fi, "FilterInvocation required");
List<ConfigAttribute> attrs = securityInterceptor.obtainSecurityMetadataSource().getAttributes(fi);
if (attrs == null) {
if (securityInterceptor.isRejectPublicInvocations()) {
return false;
}
return true;
}
if ((authentication == null) || (authentication.getAuthorities() == null)
|| authentication.getAuthorities().isEmpty()) {
return false;
}
try {
securityInterceptor.getAccessDecisionManager().decide(authentication, fi, attrs);
} catch (AccessDeniedException unauthorized) {
if (logger.isDebugEnabled()) {
logger.debug(fi.toString() + " denied for " + authentication.toString(), unauthorized);
}
return false;
}
return true;
}
public void setSecurityInterceptor(AbstractSecurityInterceptor securityInterceptor) {
Assert.notNull(securityInterceptor, "AbstractSecurityInterceptor cannot be null");
Assert.isTrue(FilterInvocation.class.equals(securityInterceptor.getSecureObjectClass()),
"AbstractSecurityInterceptor does not support FilterInvocations");
Assert.notNull(securityInterceptor.getAccessDecisionManager(),
"AbstractSecurityInterceptor must provide a non-null AccessDecisionManager");
this.securityInterceptor = securityInterceptor;
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Enforces security for HTTP requests, typically by the URL requested.
</body>
</html>
@@ -0,0 +1,90 @@
package org.springframework.security.securechannel;
import org.springframework.security.web.util.PortMapper;
import org.springframework.security.web.util.PortMapperImpl;
import org.springframework.security.web.util.PortResolver;
import org.springframework.security.web.util.PortResolverImpl;
import org.springframework.util.Assert;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Luke Taylor
* @version $Id$
*/
public abstract class AbstractRetryEntryPoint implements ChannelEntryPoint {
//~ Static fields/initializers =====================================================================================
protected final Log logger = LogFactory.getLog(getClass());
//~ Instance fields ================================================================================================
private PortMapper portMapper = new PortMapperImpl();
private PortResolver portResolver = new PortResolverImpl();
/** The scheme ("http://" or "https://") */
private String scheme;
/** The standard port for the scheme (80 for http, 443 for https) */
private int standardPort;
//~ Constructors ===================================================================================================
public AbstractRetryEntryPoint(String scheme, int standardPort) {
this.scheme = scheme;
this.standardPort = standardPort;
}
//~ Methods ========================================================================================================
public void commence(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
String pathInfo = request.getPathInfo();
String queryString = request.getQueryString();
String contextPath = request.getContextPath();
String destination = request.getServletPath() + ((pathInfo == null) ? "" : pathInfo)
+ ((queryString == null) ? "" : ("?" + queryString));
String redirectUrl = contextPath;
Integer currentPort = new Integer(portResolver.getServerPort(request));
Integer redirectPort = getMappedPort(currentPort);
if (redirectPort != null) {
boolean includePort = redirectPort.intValue() != standardPort;
redirectUrl = scheme + request.getServerName() + ((includePort) ? (":" + redirectPort) : "") + contextPath
+ destination;
}
if (logger.isDebugEnabled()) {
logger.debug("Redirecting to: " + redirectUrl);
}
((HttpServletResponse) res).sendRedirect(((HttpServletResponse) res).encodeRedirectURL(redirectUrl));
}
protected abstract Integer getMappedPort(Integer mapFromPort);
protected PortMapper getPortMapper() {
return portMapper;
}
protected PortResolver getPortResolver() {
return portResolver;
}
public void setPortMapper(PortMapper portMapper) {
Assert.notNull(portMapper, "portMapper cannot be null");
this.portMapper = portMapper;
}
public void setPortResolver(PortResolver portResolver) {
Assert.notNull(portResolver, "portResolver cannot be null");
this.portResolver = portResolver;
}
}
@@ -0,0 +1,54 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
/**
* Decides whether a web channel provides sufficient security.
*
* @author Ben Alex
* @version $Id$
*/
public interface ChannelDecisionManager {
//~ Methods ========================================================================================================
/**
* Decided whether the presented {@link FilterInvocation} provides the appropriate level of channel
* security based on the requested list of <tt>ConfigAttribute</tt>s.
*
*/
void decide(FilterInvocation invocation, List<ConfigAttribute> config) throws IOException, ServletException;
/**
* Indicates whether this <code>ChannelDecisionManager</code> is able to process the passed
* <code>ConfigAttribute</code>.<p>This allows the <code>ChannelProcessingFilter</code> to check every
* configuration attribute can be consumed by the configured <code>ChannelDecisionManager</code>.</p>
*
* @param attribute a configuration attribute that has been configured against the
* <code>ChannelProcessingFilter</code>
*
* @return true if this <code>ChannelDecisionManager</code> can support the passed configuration attribute
*/
boolean supports(ConfigAttribute attribute);
}
@@ -0,0 +1,113 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletException;
/**
* Implementation of {@link ChannelDecisionManager}.
* <p>
* Iterates through each configured {@link ChannelProcessor}. If a <code>ChannelProcessor</code> has any issue with the
* security of the request, it should cause a redirect, exception or whatever other action is appropriate for the
* <code>ChannelProcessor</code> implementation.
* <p>
* Once any response is committed (ie a redirect is written to the response object), the
* <code>ChannelDecisionManagerImpl</code> will not iterate through any further <code>ChannelProcessor</code>s.
* <p>
* The attribute "ANY_CHANNEL" if applied to a particular URL, the iteration through the channel processors will be
* skipped (see SEC-494, SEC-335).
*
* @author Ben Alex
* @version $Id$
*/
public class ChannelDecisionManagerImpl implements ChannelDecisionManager, InitializingBean {
public static final String ANY_CHANNEL = "ANY_CHANNEL";
//~ Instance fields ================================================================================================
private List<ChannelProcessor> channelProcessors;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notEmpty(channelProcessors, "A list of ChannelProcessors is required");
}
public void decide(FilterInvocation invocation, List<ConfigAttribute> config) throws IOException, ServletException {
Iterator<ConfigAttribute> attrs = config.iterator();
while (attrs.hasNext()) {
ConfigAttribute attribute = attrs.next();
if (ANY_CHANNEL.equals(attribute.getAttribute())) {
return;
}
}
for (ChannelProcessor processor : channelProcessors) {
processor.decide(invocation, config);
if (invocation.getResponse().isCommitted()) {
break;
}
}
}
protected List<ChannelProcessor> getChannelProcessors() {
return this.channelProcessors;
}
@SuppressWarnings("cast")
public void setChannelProcessors(List<?> newList) {
Assert.notEmpty(newList, "A list of ChannelProcessors is required");
channelProcessors = new ArrayList<ChannelProcessor>(newList.size());
for (Object currentObject : newList) {
Assert.isInstanceOf(ChannelProcessor.class, currentObject, "ChannelProcessor " +
currentObject.getClass().getName() + " must implement ChannelProcessor");
channelProcessors.add((ChannelProcessor)currentObject);
}
}
public boolean supports(ConfigAttribute attribute) {
if (ANY_CHANNEL.equals(attribute.getAttribute())) {
return true;
}
for (ChannelProcessor processor : channelProcessors) {
if (processor.supports(attribute)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,50 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* May be used by a {@link ChannelProcessor} to launch a web channel.
*
* <p>
* <code>ChannelProcessor</code>s can elect to launch a new web channel directly, or they can delegate to another class.
* The <code>ChannelEntryPoint</code> is a pluggable interface to assist <code>ChannelProcessor</code>s in performing
* this delegation.
*
* @author Ben Alex
* @version $Id$
*/
public interface ChannelEntryPoint {
//~ Methods ========================================================================================================
/**
* Commences a secure channel.
* <p>
* Implementations should modify the headers on the <code>ServletResponse</code> as necessary to commence the user
* agent using the implementation's supported channel type.
*
* @param request that a <code>ChannelProcessor</code> has rejected
* @param response so that the user agent can begin using a new channel
*
*/
void commence(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
}
@@ -0,0 +1,130 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.security.intercept.web.FilterInvocationSecurityMetadataSource;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.util.Assert;
/**
* Ensures a web request is delivered over the required channel.
* <p>Internally uses a {@link FilterInvocation} to represent the request, so that the
* <code>FilterInvocation</code>-related property editors and lookup classes can be used.</p>
* <p>Delegates the actual channel security decisions and necessary actions to the configured
* {@link ChannelDecisionManager}. If a response is committed by the <code>ChannelDecisionManager</code>,
* the filter chain will not proceed.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class ChannelProcessingFilter extends SpringSecurityFilter implements InitializingBean {
//~ Instance fields ================================================================================================
private ChannelDecisionManager channelDecisionManager;
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(filterInvocationSecurityMetadataSource, "filterInvocationSecurityMetadataSource must be specified");
Assert.notNull(channelDecisionManager, "channelDecisionManager must be specified");
Collection<ConfigAttribute> attrDefs = this.filterInvocationSecurityMetadataSource.getAllConfigAttributes();
if (attrDefs == null) {
if (logger.isWarnEnabled()) {
logger.warn("Could not validate configuration attributes as the FilterInvocationSecurityMetadataSource did "
+ "not return any attributes");
}
return;
}
Set<ConfigAttribute> unsupportedAttributes = new HashSet<ConfigAttribute>();
for (ConfigAttribute attr : attrDefs) {
if (!this.channelDecisionManager.supports(attr)) {
unsupportedAttributes.add(attr);
}
}
if (unsupportedAttributes.size() == 0) {
if (logger.isInfoEnabled()) {
logger.info("Validated configuration attributes");
}
} else {
throw new IllegalArgumentException("Unsupported configuration attributes: " + unsupportedAttributes);
}
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
List<ConfigAttribute> attr = this.filterInvocationSecurityMetadataSource.getAttributes(fi);
if (attr != null) {
if (logger.isDebugEnabled()) {
logger.debug("Request: " + fi.toString() + "; ConfigAttributes: " + attr);
}
channelDecisionManager.decide(fi, attr);
if (fi.getResponse().isCommitted()) {
return;
}
}
chain.doFilter(request, response);
}
public ChannelDecisionManager getChannelDecisionManager() {
return channelDecisionManager;
}
public FilterInvocationSecurityMetadataSource getFilterInvocationSecurityMetadataSource() {
return filterInvocationSecurityMetadataSource;
}
public void setChannelDecisionManager(ChannelDecisionManager channelDecisionManager) {
this.channelDecisionManager = channelDecisionManager;
}
public void setFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
this.filterInvocationSecurityMetadataSource = filterInvocationSecurityMetadataSource;
}
public int getOrder() {
return FilterChainOrder.CHANNEL_FILTER;
}
}
@@ -0,0 +1,61 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
/**
* Decides whether a web channel meets a specific security condition.
* <p>
* <code>ChannelProcessor</code> implementations are iterated by the {@link ChannelDecisionManagerImpl}.
* <p>
* If an implementation has an issue with the channel security, they should
* take action themselves. The callers of the implementation do not take any
* action.
*
* @author Ben Alex
* @version $Id$
*/
public interface ChannelProcessor {
//~ Methods ========================================================================================================
/**
* Decided whether the presented {@link FilterInvocation} provides the appropriate level of channel
* security based on the requested list of <tt>ConfigAttribute</tt>s.
*
*/
void decide(FilterInvocation invocation, List<ConfigAttribute> config) throws IOException, ServletException;
/**
* Indicates whether this <code>ChannelProcessor</code> is able to process the passed
* <code>ConfigAttribute</code>.
* <p>
* This allows the <code>ChannelProcessingFilter</code> to check every configuration attribute can be consumed
* by the configured <code>ChannelDecisionManager</code>.
*
* @param attribute a configuration attribute that has been configured against the <tt>ChannelProcessingFilter</tt>.
*
* @return true if this <code>ChannelProcessor</code> can support the passed configuration attribute
*/
boolean supports(ConfigAttribute attribute);
}
@@ -0,0 +1,93 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.util.Assert;
/**
* Ensures channel security is inactive by review of <code>HttpServletRequest.isSecure()</code> responses.
* <p>
* The class responds to one case-sensitive keyword, {@link #getInsecureKeyword}. If this keyword is detected,
* <code>HttpServletRequest.isSecure()</code> is used to determine the channel security offered. If channel security
* is present, the configured <code>ChannelEntryPoint</code> is called. By default the entry point is {@link
* RetryWithHttpEntryPoint}.
* <p>
* The default <code>insecureKeyword</code> is <code>REQUIRES_INSECURE_CHANNEL</code>.
*
* @author Ben Alex
* @version $Id$
*/
public class InsecureChannelProcessor implements InitializingBean, ChannelProcessor {
//~ Instance fields ================================================================================================
private ChannelEntryPoint entryPoint = new RetryWithHttpEntryPoint();
private String insecureKeyword = "REQUIRES_INSECURE_CHANNEL";
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(insecureKeyword, "insecureKeyword required");
Assert.notNull(entryPoint, "entryPoint required");
}
public void decide(FilterInvocation invocation, List<ConfigAttribute> config) throws IOException, ServletException {
if ((invocation == null) || (config == null)) {
throw new IllegalArgumentException("Nulls cannot be provided");
}
for (ConfigAttribute attribute : config) {
if (supports(attribute)) {
if (invocation.getHttpRequest().isSecure()) {
entryPoint.commence(invocation.getRequest(), invocation.getResponse());
}
}
}
}
public ChannelEntryPoint getEntryPoint() {
return entryPoint;
}
public String getInsecureKeyword() {
return insecureKeyword;
}
public void setEntryPoint(ChannelEntryPoint entryPoint) {
this.entryPoint = entryPoint;
}
public void setInsecureKeyword(String secureKeyword) {
this.insecureKeyword = secureKeyword;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute != null) && (attribute.getAttribute() != null)
&& attribute.getAttribute().equals(getInsecureKeyword())) {
return true;
} else {
return false;
}
}
}
@@ -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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
/**
* Commences an insecure channel by retrying the original request using HTTP.
* <p>
* This entry point should suffice in most circumstances. However, it is not intended to properly handle HTTP POSTs or
* other usage where a standard redirect would cause an issue.
*
* @author Ben Alex
* @version $Id$
*/
public class RetryWithHttpEntryPoint extends AbstractRetryEntryPoint {
public RetryWithHttpEntryPoint() {
super("http://", 80);
}
protected Integer getMappedPort(Integer mapFromPort) {
return getPortMapper().lookupHttpPort(mapFromPort);
}
}
@@ -0,0 +1,36 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
/**
* Commences a secure channel by retrying the original request using HTTPS.
* <p>
* This entry point should suffice in most circumstances. However, it is not intended to properly handle HTTP POSTs
* or other usage where a standard redirect would cause an issue.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class RetryWithHttpsEntryPoint extends AbstractRetryEntryPoint {
public RetryWithHttpsEntryPoint() {
super("https://", 443);
}
protected Integer getMappedPort(Integer mapFromPort) {
return getPortMapper().lookupHttpsPort(mapFromPort);
}
}
@@ -0,0 +1,91 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.securechannel;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.util.Assert;
/**
* Ensures channel security is active by review of <code>HttpServletRequest.isSecure()</code> responses.
* <p>
* The class responds to one case-sensitive keyword, {@link #getSecureKeyword}. If this keyword is detected,
* <code>HttpServletRequest.isSecure()</code> is used to determine the channel security offered. If channel security
* is not present, the configured <code>ChannelEntryPoint</code> is called. By default the entry point is {@link
* RetryWithHttpsEntryPoint}.
* <p>
* The default <code>secureKeyword</code> is <code>REQUIRES_SECURE_CHANNEL</code>.
*
* @author Ben Alex
* @version $Id$
*/
public class SecureChannelProcessor implements InitializingBean, ChannelProcessor {
//~ Instance fields ================================================================================================
private ChannelEntryPoint entryPoint = new RetryWithHttpsEntryPoint();
private String secureKeyword = "REQUIRES_SECURE_CHANNEL";
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(secureKeyword, "secureKeyword required");
Assert.notNull(entryPoint, "entryPoint required");
}
public void decide(FilterInvocation invocation, List<ConfigAttribute> config) throws IOException, ServletException {
Assert.isTrue((invocation != null) && (config != null), "Nulls cannot be provided");
for (ConfigAttribute attribute : config) {
if (supports(attribute)) {
if (!invocation.getHttpRequest().isSecure()) {
entryPoint.commence(invocation.getRequest(), invocation.getResponse());
}
}
}
}
public ChannelEntryPoint getEntryPoint() {
return entryPoint;
}
public String getSecureKeyword() {
return secureKeyword;
}
public void setEntryPoint(ChannelEntryPoint entryPoint) {
this.entryPoint = entryPoint;
}
public void setSecureKeyword(String secureKeyword) {
this.secureKeyword = secureKeyword;
}
public boolean supports(ConfigAttribute attribute) {
if ((attribute != null) && (attribute.getAttribute() != null)
&& attribute.getAttribute().equals(getSecureKeyword())) {
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,6 @@
<html>
<body>
Classes that ensure web requests are received over required
transport channels.
</body>
</html>
@@ -0,0 +1,169 @@
package org.springframework.security.ui;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.Authentication;
import org.springframework.security.ui.logout.LogoutHandler;
import org.springframework.security.web.util.RedirectUtils;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Base class containing the logic used by strategies which handle redirection to a URL and
* are passed an <tt>Authentication</tt> object as part of the contract.
* See {@link AuthenticationSuccessHandler} and {@link LogoutHandler}, for example.
* <p>
* Uses the following logic sequence to determine how it should handle the forward/redirect
* <ul>
* <li>
* If the <tt>alwaysUseDefaultTargetUrl</tt> property is set to true, the <tt>defaultTargetUrl</tt> property
* will be used for the destination.
* </li>
* <li>
* If a parameter matching the <tt>targetUrlParameter</tt> has been set on the request, the value will be used as
* the destination.
* </li>
* <li>
* If the <tt>useReferer</tt> property is set, the "Referer" HTTP header value will be used, if present.
* </li>
* <li>
* As a fallback option, the <tt>defaultTargetUrl</tt> value will be used.
* </li>
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public abstract class AbstractAuthenticationTargetUrlRequestHandler {
public static String DEFAULT_TARGET_PARAMETER = "spring-security-redirect";
protected final Log logger = LogFactory.getLog(this.getClass());
private String targetUrlParameter = DEFAULT_TARGET_PARAMETER;
private String defaultTargetUrl = "/";
private boolean alwaysUseDefaultTargetUrl = false;
private boolean useRelativeContext = false;
private boolean useReferer = false;
protected AbstractAuthenticationTargetUrlRequestHandler() {
}
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response);
RedirectUtils.sendRedirect(request, response, targetUrl, useRelativeContext);
}
private String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
if (isAlwaysUseDefaultTargetUrl()) {
return defaultTargetUrl;
}
// Check for the parameter and use that if available
String targetUrl = request.getParameter(targetUrlParameter);
if (StringUtils.hasText(targetUrl)) {
try {
targetUrl = URLDecoder.decode(targetUrl, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 not supported. Shouldn't be possible");
}
logger.debug("Found targetUrlParameter in request: " + targetUrl);
return targetUrl;
}
if (useReferer && !StringUtils.hasLength(targetUrl)) {
targetUrl = request.getHeader("Referer");
logger.debug("Using Referer header: " + targetUrl);
}
if (!StringUtils.hasText(targetUrl)) {
targetUrl = defaultTargetUrl;
logger.debug("Using default Url: " + targetUrl);
}
return targetUrl;
}
/**
* Supplies the default target Url that will be used if no saved request is found or the
* <tt>alwaysUseDefaultTargetUrl</tt> property is set to true. If not set, defaults to <tt>/</tt>.
*
* @return the defaultTargetUrl property
*/
protected String getDefaultTargetUrl() {
return defaultTargetUrl;
}
/**
* Supplies the default target Url that will be used if no saved request is found in the session, or the
* <tt>alwaysUseDefaultTargetUrl</tt> property is set to true. If not set, defaults to <tt>/</tt>. It
* will be treated as relative to the web-app's context path, and should include the leading <code>/</code>.
* Alternatively, inclusion of a scheme name (such as "http://" or "https://") as the prefix will denote a
* fully-qualified URL and this is also supported.
*
* @param defaultTargetUrl
*/
public void setDefaultTargetUrl(String defaultTargetUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultTargetUrl),
"defaultTarget must start with '/' or with 'http(s)'");
this.defaultTargetUrl = defaultTargetUrl;
}
/**
* If <code>true</code>, will always redirect to the value of <tt>defaultTargetUrl</tt>
* (defaults to <code>false</code>).
*/
public void setAlwaysUseDefaultTargetUrl(boolean alwaysUseDefaultTargetUrl) {
this.alwaysUseDefaultTargetUrl = alwaysUseDefaultTargetUrl;
}
protected boolean isAlwaysUseDefaultTargetUrl() {
return alwaysUseDefaultTargetUrl;
}
/**
* The current request will be checked for this parameter before and the value used as the target URL if resent.
*
* @param targetUrlParameter the name of the parameter containing the encoded target URL. Defaults
* to "redirect".
*/
public void setTargetUrlParameter(String targetUrlParameter) {
Assert.hasText("targetUrlParameter canot be null or empty");
this.targetUrlParameter = targetUrlParameter;
}
protected String getTargetUrlParameter() {
return targetUrlParameter;
}
/**
* If <tt>true</tt>, causes any redirection URLs to be calculated minus the protocol
* and context path (defaults to <tt>false</tt>).
*/
public void setUseRelativeContext(boolean useRelativeContext) {
this.useRelativeContext = useRelativeContext;
}
protected boolean isUseRelativeContext() {
return useRelativeContext;
}
/**
* If set to <tt>true</tt> the <tt>Referer</tt> header will be used (if available). Defaults to <tt>false</tt>.
*/
public void setUseReferer(boolean useReferer) {
this.useReferer = useReferer;
}
}
@@ -0,0 +1,439 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.security.concurrent.SessionRegistry;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.ui.rememberme.NullRememberMeServices;
import org.springframework.security.ui.rememberme.RememberMeServices;
import org.springframework.security.web.util.SessionUtils;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
/**
* Abstract processor of browser-based HTTP-based authentication requests.
*
* <h3>Authentication Process</h3>
*
* The filter requires that you set the <tt>authenticationManager</tt> property. An <tt>AuthenticationManager</tt> is
* required to process the authentication request tokens created by implementing classes.
* <p>
* This filter will intercept a request and attempt to perform authentication from that request if
* the request URL matches the value of the <tt>filterProcessesUrl</tt> property. This behaviour can modified by
* overriding the method {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) requiresAuthentication}.
* <p>
* Authentication is performed by the {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} method, which must be implemented by subclasses.
*
* <h4>Authentication Success</h4>
*
* If authentication is successful, the resulting {@link Authentication} object will be placed into the
* <code>SecurityContext</code> for the current thread, which is guaranteed to have already been created by an earlier
* filter. The configured {@link #setAuthenticationSuccessHandler(AuthenticationSuccessHandler) AuthenticationSuccessHandler} will
* then be called to take the redirect to the appropriate destination after a successful login. The default behaviour
* is implemented in a {@link SavedRequestAwareAuthenticationSuccessHandler} which will make use of any
* <tt>SavedRequest</tt> set by the <tt>ExceptionTranslationFilter</tt> and redirect the user to the URL contained
* therein. Otherwise it will redirect to the webapp root "/". You can customize this behaviour by injecting a
* differently configured instance of this class, or by using a different implementation.
* <p>
* See the {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, Authentication)
* successfulAuthentication} method for more information.
*
* <h4>Authentication Failure</h4>
*
* If authentication fails, the resulting <tt>AuthenticationException</tt> will be placed into the <tt>HttpSession</tt>
* with the attribute defined by {@link #SPRING_SECURITY_LAST_EXCEPTION_KEY}. It will then delegate to the configured
* {@link AuthenticationFailureHandler} to allow the failure information to be conveyed to the client.
* The default implementation is {@link SimpleUrlAuthenticationFailureHandler}, which sends a 401 error code to the
* client. It may also be configured with a failure URL as an alternative. Again you can inject whatever
* behaviour you require here.
*
* <h4>Event Publication</h4>
*
* If authentication is successful, an
* {@link org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent
* InteractiveAuthenticationSuccessEvent} will be published via the application context. No events will be published if
* authentication was unsuccessful, because this would generally be recorded via an
* <tt>AuthenticationManager</tt>-specific application event.
* <p>
* The filter has an optional attribute <tt>invalidateSessionOnSuccessfulAuthentication</tt> that will invalidate
* the current session on successful authentication. This is to protect against session fixation attacks (see
* <a href="http://en.wikipedia.org/wiki/Session_fixation">this Wikipedia article</a> for more information).
* The behaviour is turned off by default. Additionally there is a property <tt>migrateInvalidatedSessionAttributes</tt>
* which tells if on session invalidation we are to migrate all session attributes from the old session to a newly
* created one. This is turned on by default, but not used unless <tt>invalidateSessionOnSuccessfulAuthentication</tt>
* is true. If you are using this feature in combination with concurrent session control, you should set the
* <tt>sessionRegistry</tt> property to make sure that the session information is updated consistently.
*
* @author Ben Alex
* @version $Id$
*/
public abstract class AbstractProcessingFilter extends SpringSecurityFilter implements InitializingBean,
ApplicationEventPublisherAware, MessageSourceAware {
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_LAST_EXCEPTION_KEY = "SPRING_SECURITY_LAST_EXCEPTION";
//~ Instance fields ================================================================================================
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
/*
* Delay use of NullRememberMeServices until initialization so that namespace has a chance to inject
* the RememberMeServices implementation into custom implementations.
*/
private RememberMeServices rememberMeServices = null;
/**
* The URL destination that this filter intercepts and processes (usually
* something like <code>/j_spring_security_check</code>)
*/
private String filterProcessesUrl;
private boolean continueChainBeforeSuccessfulAuthentication = false;
/**
* Tells if we on successful authentication should invalidate the
* current session. This is a common guard against session fixation attacks.
* Defaults to <code>false</code>.
*/
private boolean invalidateSessionOnSuccessfulAuthentication = false;
/**
* If {@link #invalidateSessionOnSuccessfulAuthentication} is true, this
* flag indicates that the session attributes of the session to be invalidated
* are to be migrated to the new session. Defaults to <code>true</code> since
* nothing will happen unless {@link #invalidateSessionOnSuccessfulAuthentication}
* is true.
*/
private boolean migrateInvalidatedSessionAttributes = true;
private boolean allowSessionCreation = true;
private SessionRegistry sessionRegistry;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//~ Constructors ===================================================================================================
/**
* @param defaultFilterProcessesUrl the default value for <tt>filterProcessesUrl</tt>.
*/
protected AbstractProcessingFilter(String defaultFilterProcessesUrl) {
this.filterProcessesUrl = defaultFilterProcessesUrl;
}
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL");
Assert.notNull(authenticationManager, "authenticationManager must be specified");
if (rememberMeServices == null) {
rememberMeServices = new NullRememberMeServices();
}
}
/**
* Invokes the {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse) requiresAuthentication}
* method to determine whether the request is for authentication and should be handled by this filter.
* If it is an authentication request, the
* {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse) attemptAuthentication} will be invoked
* to perform the authentication. There are then three possible outcomes:
* <ol>
* <li>An <tt>Authentication</tt> object is returned.
* The {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, Authentication)
* successfulAuthentication} method will be invoked</li>
* <li>An <tt>AuthenticationException</tt> occurs during authentication.
* The {@link #unSuccessfulAuthentication(HttpServletRequest, HttpServletResponse, Authentication)
* unSuccessfulAuthentication} method will be invoked</li>
* <li>Null is returned, indicating that the authentication process is incomplete.
* The method will then return immediately, assuming that the subclass has done any necessary work (such as
* redirects) to continue the authentication process. The assumption is that a later request will be received
* by this method where the returned <tt>Authentication</tt> object is not null.
* </ol>
*/
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed authentication
return;
}
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, authResult);
}
/**
* Indicates whether this filter should attempt to process a login request for the current invocation.
* <p>
* It strips any parameters from the "path" section of the request URL (such
* as the jsessionid parameter in
* <em>http://host/myapp/index.html;jsessionid=blah</em>) before matching
* against the <code>filterProcessesUrl</code> property.
* <p>
* Subclasses may override for special requirements, such as Tapestry integration.
*
* @return <code>true</code> if the filter should attempt authentication, <code>false</code> otherwise.
*/
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
if ("".equals(request.getContextPath())) {
return uri.endsWith(filterProcessesUrl);
}
return uri.endsWith(request.getContextPath() + filterProcessesUrl);
}
/**
* Performs actual authentication.
* <p>
* The implementation should do one of the following:
* <ol>
* <li>Return a populated authentication token for the authenticated user, indicating successful authentication</li>
* <li>Return null, indicating that the authentication process is still in progress. Before returning, the
* implementation should perform any additional work required to complete the process.</li>
* <li>Throw an <tt>AuthenticationException</tt> if the authentication process fails</li>
* </ol>
*
* @param request from which to extract parameters and perform the authentication
* @param response the response, which may be needed if the implementation has to do a redirect as part of a
* multi-stage authentication process (such as OpenID).
*
* @return the authenticated user token, or null if authentication is incomplete.
*
* @throws AuthenticationException if authentication fails.
*/
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the {@link SecurityContextHolder}</li>
* <li>Performs any configured session migration behaviour</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* @param authResult the object returned from the <tt>attemptAuthentication</tt> method.
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
if (invalidateSessionOnSuccessfulAuthentication) {
SessionUtils.startNewSessionIfRequired(request, migrateInvalidatedSessionAttributes, sessionRegistry);
}
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
/**
* Default behaviour for unsuccessful authentication.
* <ol>
* <li>Clears the {@link SecurityContextHolder}</li>
* <li>Stores the exception in the session (if it exists or <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
* <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
* </ol>
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString());
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler" + failureHandler);
}
try {
HttpSession session = request.getSession(false);
if (session != null || allowSessionCreation) {
request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed);
}
}
catch (Exception ignored) {
}
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public String getFilterProcessesUrl() {
return filterProcessesUrl;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
public RememberMeServices getRememberMeServices() {
return rememberMeServices;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
this.rememberMeServices = rememberMeServices;
}
/**
* Indicates if the filter chain should be continued prior to delegation to
* {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse,
* Authentication)}, which may be useful in certain environment (such as
* Tapestry applications). Defaults to <code>false</code>.
*/
public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setInvalidateSessionOnSuccessfulAuthentication(boolean invalidateSessionOnSuccessfulAuthentication) {
this.invalidateSessionOnSuccessfulAuthentication = invalidateSessionOnSuccessfulAuthentication;
}
public void setMigrateInvalidatedSessionAttributes(boolean migrateInvalidatedSessionAttributes) {
this.migrateInvalidatedSessionAttributes = migrateInvalidatedSessionAttributes;
}
public AuthenticationDetailsSource getAuthenticationDetailsSource() {
// Required due to SEC-310
return authenticationDetailsSource;
}
protected boolean getAllowSessionCreation() {
return allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
/**
* The session registry needs to be set if session fixation attack protection is in use (and concurrent
* session control is enabled).
*/
public void setSessionRegistry(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
/**
* Sets the strategy used to handle a successful authentication.
* By default a {@link SavedRequestAwareAuthenticationSuccessHandler} is used.
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
}
@@ -0,0 +1,49 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import org.springframework.security.AccessDeniedException;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Used by {@link ExceptionTranslationFilter} to handle an
* <code>AccessDeniedException</code>.
*
* @author Ben Alex
* @version $Id$
*/
public interface AccessDeniedHandler {
//~ Methods ========================================================================================================
/**
* Handles an access denied failure.
*
* @param request that resulted in an <code>AccessDeniedException</code>
* @param response so that the user agent can be advised of the failure
* @param accessDeniedException that caused the invocation
*
* @throws IOException in the event of an IOException
* @throws ServletException in the event of a ServletException
*/
void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException;
}
@@ -0,0 +1,88 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.AccessDeniedException;
/**
* Base implementation of {@link AccessDeniedHandler}.
* <p>
* This implementation sends a 403 (SC_FORBIDDEN) HTTP error code. In addition, if an {@link #errorPage} is defined,
* the implementation will perform a request dispatcher "forward" to the specified error page view.
* Being a "forward", the <code>SecurityContextHolder</code> will remain
* populated. This is of benefit if the view (or a tag library or macro) wishes to access the
* <code>SecurityContextHolder</code>. The request scope will also be populated with the exception itself, available
* from the key {@link #SPRING_SECURITY_ACCESS_DENIED_EXCEPTION_KEY}.
*
* @author Ben Alex
* @version $Id$
*/
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_ACCESS_DENIED_EXCEPTION_KEY = "SPRING_SECURITY_403_EXCEPTION";
protected static final Log logger = LogFactory.getLog(AccessDeniedHandlerImpl.class);
//~ Instance fields ================================================================================================
private String errorPage;
//~ Methods ========================================================================================================
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException {
if (!response.isCommitted()) {
if (errorPage != null) {
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(SPRING_SECURITY_ACCESS_DENIED_EXCEPTION_KEY, accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// forward to error page.
RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
dispatcher.forward(request, response);
} else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
}
}
}
/**
* The error page to use. Must begin with a "/" and is interpreted relative to the current context root.
*
* @param errorPage the dispatcher path to display
*
* @throws IllegalArgumentException if the argument doesn't comply with the above limitations
*/
public void setErrorPage(String errorPage) {
if ((errorPage != null) && !errorPage.startsWith("/")) {
throw new IllegalArgumentException("errorPage must begin with '/'");
}
this.errorPage = errorPage;
}
}
@@ -0,0 +1,53 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import org.springframework.security.AuthenticationException;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Used by {@link ExceptionTranslationFilter} to commence an authentication scheme.
*
* @author Ben Alex
* @version $Id$
*/
public interface AuthenticationEntryPoint {
//~ Methods ========================================================================================================
/**
* Commences an authentication scheme.
* <p>
* <code>ExceptionTranslationFilter</code> will populate the <code>HttpSession</code> attribute named
* <code>AbstractProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY</code> with the requested target URL before
* calling this method.
* <p>
* Implementations should modify the headers on the <code>ServletResponse</code> as necessary to
* commence the authentication process.
*
* @param request that resulted in an <code>AuthenticationException</code>
* @param response so that the user agent can begin authentication
* @param authException that caused the invocation
*
*/
void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException;
}
@@ -0,0 +1,34 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.AuthenticationException;
import org.springframework.security.CredentialsExpiredException;
/**
* Strategy used to handle a failed authentication attempt.
* <p>
* Typical behaviour might be to redirect the user to the authentication page (in the case of a form login) to
* allow them to try again. More sophisticated logic might be implemented depending on the type of the exception.
* For example, a {@link CredentialsExpiredException} might cause a redirect to a web controller which allowed the
* user to change their password.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public interface AuthenticationFailureHandler {
/**
* Called when an authentication attempt fails.
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication request.
*/
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException;
}
@@ -0,0 +1,36 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.Authentication;
/**
* Strategy used to handle a successful user authentication.
* <p>
* Implementations can do whatever they want but typical behaviour would be to control the navigation to the
* subsequent destination (using a redirect or a forward). For example, after a user has logged in by submitting a
* login form, the application needs to decide where they should be redirected to afterwards
* (see {@link AbstractProcessingFilter} and subclasses). Other logic may also be included if required.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
* @see
*/
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
@@ -0,0 +1,63 @@
package org.springframework.security.ui;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.AuthenticationException;
import org.springframework.security.web.util.RedirectUtils;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
/**
* Uses the internal map of exceptions types to URLs to determine the destination on authentication failure. The keys
* are the full exception class names.
* <p>
* If a match isn't found, falls back to the behaviour of the parent class,
* {@link SimpleUrlAuthenticationFailureHandler}.
* <p>
* The map of exception names to URLs should be injected by setting the <tt>exceptionMappings</tt> property.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class ExceptionMappingAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Map<String, String> failureUrlMap = new HashMap<String, String>();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String url = failureUrlMap.get(exception.getClass().getName());
if (url != null) {
RedirectUtils.sendRedirect(request, response, url, isUseRelativeContext());
} else {
super.onAuthenticationFailure(request, response, exception);
}
}
/**
* Sets the map of exception types (by name) to URLs.
*
* @param failureUrlMap the map keyed by the fully-qualified name of the exception class, with the corresponding
* failure URL as the value.
*
* @throws IllegalArgumentException if the entries are not Strings or the URL is not valid.
*/
public void setExceptionMappings(Map<?,?> failureUrlMap) {
this.failureUrlMap.clear();
for (Map.Entry<?,?> entry : failureUrlMap.entrySet()) {
Object exception = entry.getKey();
Object url = entry.getValue();
Assert.isInstanceOf(String.class, exception, "Exception key must be a String (the exception classname).");
Assert.isInstanceOf(String.class, url, "URL must be a String");
Assert.isTrue(UrlUtils.isValidRedirectUrl((String)url), "Not a valid redirect URL: " + url);
this.failureUrlMap.put((String)exception, (String)url);
}
}
}
@@ -0,0 +1,271 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.SpringSecurityException;
import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.InsufficientAuthenticationException;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.ui.savedrequest.SavedRequest;
import org.springframework.security.util.ThrowableAnalyzer;
import org.springframework.security.util.ThrowableCauseExtractor;
import org.springframework.security.web.util.PortResolver;
import org.springframework.security.web.util.PortResolverImpl;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Handles any <code>AccessDeniedException</code> and <code>AuthenticationException</code> thrown within the
* filter chain.
* <p>
* This filter is necessary because it provides the bridge between Java exceptions and HTTP responses.
* It is solely concerned with maintaining the user interface. This filter does not do any actual security enforcement.
* <p>
* If an {@link AuthenticationException} is detected, the filter will launch the <code>authenticationEntryPoint</code>.
* This allows common handling of authentication failures originating from any subclass of
* {@link org.springframework.security.intercept.AbstractSecurityInterceptor}.
* <p>
* If an {@link AccessDeniedException} is detected, the filter will determine whether or not the user is an anonymous
* user. If they are an anonymous user, the <code>authenticationEntryPoint</code> will be launched. If they are not
* an anonymous user, the filter will delegate to the {@link org.springframework.security.ui.AccessDeniedHandler}.
* By default the filter will use {@link org.springframework.security.ui.AccessDeniedHandlerImpl}.
* <p>
* To use this filter, it is necessary to specify the following properties:
* <ul>
* <li><code>authenticationEntryPoint</code> indicates the handler that
* should commence the authentication process if an
* <code>AuthenticationException</code> is detected. Note that this may also
* switch the current protocol from http to https for an SSL login.</li>
* <li><code>portResolver</code> is used to determine the "real" port that a
* request was received on.</li>
* </ul>
*
* @author Ben Alex
* @author colin sampaleanu
* @version $Id$
*/
public class ExceptionTranslationFilter extends SpringSecurityFilter implements InitializingBean {
//~ Instance fields ================================================================================================
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private PortResolver portResolver = new PortResolverImpl();
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
private boolean createSessionAllowed = true;
private boolean justUseSavedRequestOnGet;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint must be specified");
Assert.notNull(portResolver, "portResolver must be specified");
Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must be specified");
Assert.notNull(throwableAnalyzer, "throwableAnalyzer must be specified");
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
ServletException {
try {
chain.doFilter(request, response);
if (logger.isDebugEnabled()) {
logger.debug("Chain processed normally");
}
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
SpringSecurityException ase = (SpringSecurityException)
this.throwableAnalyzer.getFirstThrowableOfType(SpringSecurityException.class, causeChain);
if (ase != null) {
handleException(request, response, chain, ase);
}
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. These are not expected to happen
throw new RuntimeException(ex);
}
}
}
public AuthenticationEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}
public AuthenticationTrustResolver getAuthenticationTrustResolver() {
return authenticationTrustResolver;
}
public PortResolver getPortResolver() {
return portResolver;
}
private void handleException(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
SpringSecurityException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
}
sendStartAuthentication(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
if (authenticationTrustResolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication())) {
if (logger.isDebugEnabled()) {
logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point",
exception);
}
sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
}
accessDeniedHandler.handle(request, response, (AccessDeniedException) exception);
}
}
}
/**
* If <code>true</code>, indicates that <code>ExceptionTranslationFilter</code> is permitted to store the target
* URL and exception information in a new <code>HttpSession</code> (the default).
* In situations where you do not wish to unnecessarily create <code>HttpSession</code>s - because the user agent
* will know the failed URL, such as with BASIC or Digest authentication - you may wish to set this property to
* <code>false</code>.
* <p>
* Remember to also set
* {@link org.springframework.security.context.web.HttpSessionSecurityContextRepository#setAllowSessionCreation(boolean)}
* to <code>false</code> if you set this property to <code>false</code>.
*
* @return <code>true</code> if the <code>HttpSession</code> will be
* used to store information about the failed request, <code>false</code>
* if the <code>HttpSession</code> will not be used
*/
public boolean isCreateSessionAllowed() {
return createSessionAllowed;
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
saveRequestIfAllowed(request);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
private void saveRequestIfAllowed(HttpServletRequest request) {
if (!justUseSavedRequestOnGet || "GET".equals(request.getMethod())) {
SavedRequest savedRequest = new SavedRequest(request, portResolver);
if (createSessionAllowed || request.getSession(false) != null) {
// Store the HTTP request itself. Used by AbstractProcessingFilter
// for redirection after successful authentication (SEC-29)
request.getSession().setAttribute(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY, savedRequest);
logger.debug("SavedRequest added to Session: " + savedRequest);
}
}
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "AccessDeniedHandler required");
this.accessDeniedHandler = accessDeniedHandler;
}
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
this.authenticationTrustResolver = authenticationTrustResolver;
}
public void setCreateSessionAllowed(boolean createSessionAllowed) {
this.createSessionAllowed = createSessionAllowed;
}
public void setPortResolver(PortResolver portResolver) {
this.portResolver = portResolver;
}
public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
this.throwableAnalyzer = throwableAnalyzer;
}
/**
* If <code>true</code>, will only use <code>SavedRequest</code> to determine the target URL on successful
* authentication if the request that caused the authentication request was a GET. Defaults to false.
*/
public void setJustUseSavedRequestOnGet(boolean justUseSavedRequestOnGet) {
this.justUseSavedRequestOnGet = justUseSavedRequestOnGet;
}
public int getOrder() {
return FilterChainOrder.EXCEPTION_TRANSLATION_FILTER;
}
/**
* Default implementation of <code>ThrowableAnalyzer</code> which is capable of also unwrapping
* <code>ServletException</code>s.
*/
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
/**
* @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
*/
protected void initExtractorMap() {
super.initExtractorMap();
registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
public Throwable extractCause(Throwable throwable) {
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
return ((ServletException) throwable).getRootCause();
}
});
}
}
}
@@ -0,0 +1,77 @@
package org.springframework.security.ui;
import org.springframework.util.Assert;
import java.util.Map;
import java.util.LinkedHashMap;
/**
* Stores the default order numbers of all Spring Security filters for use in configuration.
*
* @author Luke Taylor
* @version $Id$
*/
public abstract class FilterChainOrder {
/**
* The first position at which a Spring Security filter will be found. Any filter with an order less than this will
* be guaranteed to be placed before the Spring Security filters in the stack.
*/
public static final int FILTER_CHAIN_FIRST = 0;
private static final int INTERVAL = 100;
private static int i = 1;
public static final int CHANNEL_FILTER = FILTER_CHAIN_FIRST;
public static final int CONCURRENT_SESSION_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int SECURITY_CONTEXT_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int HTTP_SESSION_CONTEXT_FILTER = SECURITY_CONTEXT_FILTER;
public static final int LOGOUT_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int X509_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int PRE_AUTH_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int CAS_PROCESSING_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int AUTHENTICATION_PROCESSING_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int OPENID_PROCESSING_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int LOGIN_PAGE_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int DIGEST_PROCESSING_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int BASIC_PROCESSING_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int SERVLET_API_SUPPORT_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int REMEMBER_ME_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int ANONYMOUS_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int EXCEPTION_TRANSLATION_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int NTLM_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int SESSION_FIXATION_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int FILTER_SECURITY_INTERCEPTOR = FILTER_CHAIN_FIRST + INTERVAL * i++;
public static final int SWITCH_USER_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
private static final Map<String, Integer> filterNameToOrder = new LinkedHashMap<String, Integer>();
static {
filterNameToOrder.put("FIRST", new Integer(Integer.MIN_VALUE));
filterNameToOrder.put("CHANNEL_FILTER", new Integer(CHANNEL_FILTER));
filterNameToOrder.put("CONCURRENT_SESSION_FILTER", new Integer(CONCURRENT_SESSION_FILTER));
filterNameToOrder.put("LOGOUT_FILTER", new Integer(LOGOUT_FILTER));
filterNameToOrder.put("X509_FILTER", new Integer(X509_FILTER));
filterNameToOrder.put("PRE_AUTH_FILTER", new Integer(PRE_AUTH_FILTER));
filterNameToOrder.put("CAS_PROCESSING_FILTER", new Integer(CAS_PROCESSING_FILTER));
filterNameToOrder.put("AUTHENTICATION_PROCESSING_FILTER", new Integer(AUTHENTICATION_PROCESSING_FILTER));
filterNameToOrder.put("OPENID_PROCESSING_FILTER", new Integer(OPENID_PROCESSING_FILTER));
filterNameToOrder.put("BASIC_PROCESSING_FILTER", new Integer(BASIC_PROCESSING_FILTER));
filterNameToOrder.put("SERVLET_API_SUPPORT_FILTER", new Integer(SERVLET_API_SUPPORT_FILTER));
filterNameToOrder.put("REMEMBER_ME_FILTER", new Integer(REMEMBER_ME_FILTER));
filterNameToOrder.put("ANONYMOUS_FILTER", new Integer(ANONYMOUS_FILTER));
filterNameToOrder.put("EXCEPTION_TRANSLATION_FILTER", new Integer(EXCEPTION_TRANSLATION_FILTER));
filterNameToOrder.put("NTLM_FILTER", new Integer(NTLM_FILTER));
filterNameToOrder.put("SESSION_CONTEXT_INTEGRATION_FILTER", new Integer(HTTP_SESSION_CONTEXT_FILTER));
filterNameToOrder.put("FILTER_SECURITY_INTERCEPTOR", new Integer(FILTER_SECURITY_INTERCEPTOR));
filterNameToOrder.put("SWITCH_USER_FILTER", new Integer(SWITCH_USER_FILTER));
filterNameToOrder.put("LAST", new Integer(Integer.MAX_VALUE));
}
/** Allows filters to be used by name in the XSD file without explicit reference to Java constants */
public static int getOrder(String filterName) {
Integer order = filterNameToOrder.get(filterName);
Assert.notNull(order, "Unable to match filter name " + filterName);
return order.intValue();
}
}
@@ -0,0 +1,29 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.Authentication;
import org.springframework.security.ui.logout.LogoutFilter;
/**
* Strategy that is called after a successful logout by the {@link LogoutFilter}, to handle redirection or
* forwarding to the appropriate destination.
* <p>
* Note that the interface is almost the same as {@link LogoutHandler} but may raise an exception.
* <tt>LogoutHandler</tt> implementations expect to be invoked to perform necessary cleanup, so should not throw
* exceptions.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public interface LogoutSuccessHandler {
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException;
}
@@ -0,0 +1,94 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.Authentication;
import org.springframework.security.ui.savedrequest.SavedRequest;
import org.springframework.security.web.util.RedirectUtils;
import org.springframework.security.wrapper.SavedRequestAwareWrapper;
import org.springframework.util.StringUtils;
/**
* An authentication success strategy which can make use of the {@link SavedRequest} which may have been stored in
* the session by the {@link ExceptionTranslationFilter}. When such a request is intercepted and requires authentication,
* the request data is stored to record the original destination before the authentication process commenced, and to
* allow the request to be reconstructed when a redirect to the same URL occurs. This class is responsible for
* performing the redirect to the original URL if appropriate.
* <p>
* Following a successful authentication, it decides on the redirect destination, based on the following scenarios:
* <ul>
* <li>
* If the <tt>alwaysUseDefaultTargetUrl</tt> property is set to true, the <tt>defaultTargetUrl</tt>
* will be used for the destination. Any <tt>SavedRequest</tt> stored in the session will be
* removed.
* </li>
* <li>
* If the <tt>targetUrlParameter</tt> has been set on the request, the value will be used as the destination.
* Any <tt>SavedRequest</tt> will again be removed.
* </li>
* <li>
* If a {@link SavedRequest} is found in the session (as set by the {@link ExceptionTranslationFilter} to record
* the original destination before the authentication process commenced), a redirect will be performed to the
* Url of that original destination. The <tt>SavedRequest</tt> object will remain in the session and be picked up
* when the redirected request is received (See {@link SavedRequestAwareWrapper}).
* </li>
* <li>
* If no <tt>SavedRequest</tt> is found in the session, it will delegate to the base class.
* </li>
* </ul>
*
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = getSavedRequest(request);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
if (isAlwaysUseDefaultTargetUrl() || StringUtils.hasText(request.getParameter(getTargetUrlParameter()))) {
removeSavedRequest(request);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
// Use the SavedRequest URL
String targetUrl = savedRequest.getFullRequestUrl();
logger.debug("Redirecting to SavedRequest Url: " + targetUrl);
RedirectUtils.sendRedirect(request, response, targetUrl, isUseRelativeContext());
}
private SavedRequest getSavedRequest(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
return (SavedRequest) session.getAttribute(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY);
}
return null;
}
private void removeSavedRequest(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Removing SavedRequest from session if present");
session.removeAttribute(SavedRequest.SPRING_SECURITY_SAVED_REQUEST_KEY);
}
}
}
@@ -0,0 +1,97 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationTrustResolver;
import org.springframework.security.AuthenticationTrustResolverImpl;
import org.springframework.security.concurrent.SessionRegistry;
import org.springframework.security.context.SecurityContext;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.context.web.HttpSessionSecurityContextRepository;
import org.springframework.security.web.util.SessionUtils;
/**
* Detects that a user has been authenticated since the start of the request and starts a new session.
* <p>
* This is essentially a generalization of the functionality that was implemented for SEC-399.
* Additionally, it will update the configured SessionRegistry if one is in use, thus preventing problems when used
* with Spring Security's concurrent session control.
*
* @author Martin Algesten
* @author Luke Taylor
* @since 2.0
*/
public class SessionFixationProtectionFilter extends SpringSecurityFilter {
//~ Static fields/initializers =====================================================================================
static final String FILTER_APPLIED = "__spring_security_session_fixation_filter_applied";
//~ Instance fields ================================================================================================
private SessionRegistry sessionRegistry;
/**
* Indicates that the session attributes of the session to be invalidated
* should be migrated to the new session. Defaults to <code>true</code>.
*/
private boolean migrateSessionAttributes = true;
private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Session fixation isn't a problem if there's no session
if(request.getSession(false) == null || request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
HttpSession session = request.getSession();
SecurityContext sessionSecurityContext =
(SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if (sessionSecurityContext == null && isAuthenticated()) {
// The user has been authenticated during the current request, so do the session migration
startNewSessionIfRequired(request, response);
}
chain.doFilter(request, response);
}
private boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && !authenticationTrustResolver.isAnonymous(authentication);
}
public void setMigrateSessionAttributes(boolean migrateSessionAttributes) {
this.migrateSessionAttributes = migrateSessionAttributes;
}
public void setSessionRegistry(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
public int getOrder() {
return FilterChainOrder.SESSION_FIXATION_FILTER;
}
/**
* Called when the a user wasn't authenticated at the start of the request but has been during it
* <p>
* A new session will be created, the session attributes copied to it (if
* <tt>migrateSessionAttributes</tt> is set) and the sessionRegistry updated with the new session information.
*/
protected void startNewSessionIfRequired(HttpServletRequest request, HttpServletResponse response) {
SessionUtils.startNewSessionIfRequired(request, migrateSessionAttributes, sessionRegistry);
}
}
@@ -0,0 +1,86 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.AuthenticationException;
import org.springframework.security.web.util.RedirectUtils;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
/**
* <tt>AuthenticationFailureHandler</tt> which performs a redirect to the value of the {@link #setDefaultFailureUrl
* defaultFailureUrl} property when the <tt>onAuthenticationFailure</tt> method is called.
* If the property has not been set it will send a 401 response to the client, with the error message from the
* <tt>AuthenticationException</tt> which caused the failure.
* <p>
* If the <tt>forwardToDestination</tt> parameter is set, a <tt>RequestDispatcher.forward</tt> call will be made to
* the destination instead of a redirect.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean useRelativeContext = false;
public SimpleUrlAuthenticationFailureHandler() {
}
public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
setDefaultFailureUrl(defaultFailureUrl);
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (defaultFailureUrl == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage());
} else {
if (forwardToDestination) {
request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
} else {
RedirectUtils.sendRedirect(request, response, defaultFailureUrl, useRelativeContext);
}
}
}
/**
* The URL which will be used as the failure destination.
*
* @param defaultFailureUrl the failure URL, for example "/loginFailed.jsp".
*/
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl));
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return forwardToDestination;
}
/**
* If set to <tt>true</tt>, performs a forward to the failure destination URL instead of a redirect. Defaults to
* <tt>false</tt>.
*/
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
protected boolean isUseRelativeContext() {
return useRelativeContext;
}
/**
* If true, causes any redirection URLs to be calculated minus the protocol
* and context path (defaults to false).
*/
public void setUseRelativeContext(boolean useRelativeContext) {
this.useRelativeContext = useRelativeContext;
}
}
@@ -0,0 +1,25 @@
package org.springframework.security.ui;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.Authentication;
public class SimpleUrlAuthenticationSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {
public SimpleUrlAuthenticationSuccessHandler() {
}
public SimpleUrlAuthenticationSuccessHandler(String defaultTargetUrl) {
setDefaultTargetUrl(defaultTargetUrl);
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
handle(request, response, authentication);
}
}
@@ -0,0 +1,52 @@
package org.springframework.security.ui;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.FilterChain;
import javax.servlet.ServletResponse;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.Filter;
import java.io.IOException;
/**
* Implements Ordered interface as required by security namespace configuration and implements unused filter
* lifecycle methods and performs casting of request and response to http versions in doFilter method.
*
* @author Luke Taylor
* @version $Id$
*/
public abstract class SpringSecurityFilter implements Filter, Ordered {
protected final Log logger = LogFactory.getLog(this.getClass());
/**
* Does nothing. We use IoC container lifecycle services instead.
*
* @param filterConfig ignored
* @throws ServletException ignored
*/
public final void init(FilterConfig filterConfig) throws ServletException {
}
/**
* Does nothing. We use IoC container lifecycle services instead.
*/
public final void destroy() {
}
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
doFilterHttp((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
protected abstract void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException;
public String toString() {
return getClass().getName() + "[ order=" + getOrder() + "; ]";
}
}
@@ -0,0 +1,142 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import org.springframework.security.concurrent.SessionIdentifierAware;
import java.io.Serializable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* A holder of selected HTTP details related to a web authentication request.
*
* @author Ben Alex
* @version $Id$
*/
public class WebAuthenticationDetails implements SessionIdentifierAware, Serializable {
//~ Instance fields ================================================================================================
private String remoteAddress;
private String sessionId;
//~ Constructors ===================================================================================================
/**
* Records the remote address and will also set the session Id if a session
* already exists (it won't create one).
*
* @param request that the authentication request was received from
*/
public WebAuthenticationDetails(HttpServletRequest request) {
this.remoteAddress = request.getRemoteAddr();
HttpSession session = request.getSession(false);
this.sessionId = (session != null) ? session.getId() : null;
doPopulateAdditionalInformation(request);
}
//~ Methods ========================================================================================================
/**
* Provided so that subclasses can populate additional information.
*
* @param request that the authentication request was received from
*/
protected void doPopulateAdditionalInformation(HttpServletRequest request) {}
public boolean equals(Object obj) {
if (obj instanceof WebAuthenticationDetails) {
WebAuthenticationDetails rhs = (WebAuthenticationDetails) obj;
if ((remoteAddress == null) && (rhs.getRemoteAddress() != null)) {
return false;
}
if ((remoteAddress != null) && (rhs.getRemoteAddress() == null)) {
return false;
}
if (remoteAddress != null) {
if (!remoteAddress.equals(rhs.getRemoteAddress())) {
return false;
}
}
if ((sessionId == null) && (rhs.getSessionId() != null)) {
return false;
}
if ((sessionId != null) && (rhs.getSessionId() == null)) {
return false;
}
if (sessionId != null) {
if (!sessionId.equals(rhs.getSessionId())) {
return false;
}
}
return true;
}
return false;
}
/**
* Indicates the TCP/IP address the authentication request was received from.
*
* @return the address
*/
public String getRemoteAddress() {
return remoteAddress;
}
/**
* Indicates the <code>HttpSession</code> id the authentication request was received from.
*
* @return the session ID
*/
public String getSessionId() {
return sessionId;
}
public int hashCode() {
int code = 7654;
if (this.remoteAddress != null) {
code = code * (this.remoteAddress.hashCode() % 7);
}
if (this.sessionId != null) {
code = code * (this.sessionId.hashCode() % 7);
}
return code;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString() + ": ");
sb.append("RemoteIpAddress: " + this.getRemoteAddress() + "; ");
sb.append("SessionId: " + this.getSessionId());
return sb.toString();
}
}
@@ -0,0 +1,70 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import javax.servlet.http.HttpServletRequest;
/**
* Implementation of {@link AuthenticationDetailsSource} which builds the details object from
* an <tt>HttpServletRequest</tt> object.
* <p>
* By default will create an instance of <code>WebAuthenticationDetails</code>. Any object that accepts a
* <code>HttpServletRequest</code> as its sole constructor can be used instead of this default.
*
* @author Ben Alex
* @version $Id$
*/
public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource {
//~ Instance fields ================================================================================================
private Class<?> clazz = WebAuthenticationDetails.class;
//~ Methods ========================================================================================================
/**
* @param context the <tt>HttpServletRequest</tt> object.
*/
public Object buildDetails(Object context) {
Assert.isInstanceOf(HttpServletRequest.class, context);
try {
Constructor<?> constructor = clazz.getConstructor(HttpServletRequest.class);
return constructor.newInstance(context);
} catch (NoSuchMethodException ex) {
ReflectionUtils.handleReflectionException(ex);
} catch (InvocationTargetException ex) {
ReflectionUtils.handleReflectionException(ex);
} catch (InstantiationException ex) {
ReflectionUtils.handleReflectionException(ex);
} catch (IllegalAccessException ex) {
ReflectionUtils.handleReflectionException(ex);
}
return null;
}
public void setClazz(Class<?> clazz) {
Assert.notNull(clazz, "Class required");
this.clazz = clazz;
}
}
@@ -0,0 +1,157 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.anonymous;
import org.springframework.security.Authentication;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.anonymous.AnonymousAuthenticationToken;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.security.ui.WebAuthenticationDetailsSource;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.userdetails.memory.UserAttribute;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Detects if there is no <code>Authentication</code> object in the <code>SecurityContextHolder</code>, and
* populates it with one if needed.
*
* @author Ben Alex
* @version $Id$
*/
public class AnonymousProcessingFilter extends SpringSecurityFilter implements InitializingBean {
//~ Instance fields ================================================================================================
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String key;
private UserAttribute userAttribute;
private boolean removeAfterRequest = true;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(userAttribute);
Assert.hasLength(key);
}
/**
* Enables subclasses to determine whether or not an anonymous authentication token should be setup for
* this request. This is useful if anonymous authentication should be allowed only for specific IP subnet ranges
* etc.
*
* @param request to assist the method determine request details
*
* @return <code>true</code> if the anonymous token should be setup for this request (provided that the request
* doesn't already have some other <code>Authentication</code> inside it), or <code>false</code> if no
* anonymous token should be setup for this request
*/
protected boolean applyAnonymousForThisRequest(HttpServletRequest request) {
return true;
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, userAttribute.getPassword(),
userAttribute.getAuthorities());
auth.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
return auth;
}
protected void doFilterHttp(HttpServletRequest request,HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean addedToken = false;
if (applyAnonymousForThisRequest(request)) {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(createAuthentication(request));
addedToken = true;
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
}
try {
chain.doFilter(request, response);
} finally {
if (addedToken && removeAfterRequest
&& createAuthentication(request).equals(SecurityContextHolder.getContext().getAuthentication())) {
SecurityContextHolder.getContext().setAuthentication(null);
}
}
}
public int getOrder() {
return FilterChainOrder.ANONYMOUS_FILTER;
}
public String getKey() {
return key;
}
public UserAttribute getUserAttribute() {
return userAttribute;
}
public boolean isRemoveAfterRequest() {
return removeAfterRequest;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setKey(String key) {
this.key = key;
}
/**
* Controls whether the filter will remove the Anonymous token after the request is complete. Generally
* this is desired to avoid the expense of a session being created by {@link
* org.springframework.security.context.web.HttpSessionContextIntegrationFilter HttpSessionContextIntegrationFilter} simply to
* store the Anonymous authentication token.<p>Defaults to <code>true</code>, being the most optimal and
* appropriate option (ie <code>AnonymousProcessingFilter</code> will clear the token at the end of each request,
* thus avoiding the session creation overhead in a typical configuration.</p>
*
* @param removeAfterRequest DOCUMENT ME!
*/
public void setRemoveAfterRequest(boolean removeAfterRequest) {
this.removeAfterRequest = removeAfterRequest;
}
public void setUserAttribute(UserAttribute userAttributeDefinition) {
this.userAttribute = userAttributeDefinition;
}
}
@@ -0,0 +1,261 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.basicauth;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.providers.anonymous.AnonymousAuthenticationToken;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.security.ui.WebAuthenticationDetailsSource;
import org.springframework.security.ui.AuthenticationEntryPoint;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.ui.rememberme.NullRememberMeServices;
import org.springframework.security.ui.rememberme.RememberMeServices;
import org.springframework.util.Assert;
/**
* Processes a HTTP request's BASIC authorization headers, putting the result into the
* <code>SecurityContextHolder</code>.
*
* <p>
* For a detailed background on what this filter is designed to process, refer to
* <a href="http://www.faqs.org/rfcs/rfc1945.html">RFC 1945, Section 11.1</a>. Any realm name presented in
* the HTTP request is ignored.
*
* <p>
* In summary, this filter is responsible for processing any request that has a HTTP request header of
* <code>Authorization</code> with an authentication scheme of <code>Basic</code> and a Base64-encoded
* <code>username:password</code> token. For example, to authenticate user "Aladdin" with password "open sesame" the
* following header would be presented:
* <pre>
*
* Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
* </pre>
*
* <p>
* This filter can be used to provide BASIC authentication services to both remoting protocol clients (such as
* Hessian and SOAP) as well as standard user agents (such as Internet Explorer and Netscape).
* <p>
* If authentication is successful, the resulting {@link Authentication} object will be placed into the
* <code>SecurityContextHolder</code>.
*
* <p>
* If authentication fails and <code>ignoreFailure</code> is <code>false</code> (the default), an {@link
* AuthenticationEntryPoint} implementation is called (unless the <tt>ignoreFailure</tt> property is set to
* <tt>true</tt>). Usually this should be {@link BasicProcessingFilterEntryPoint}, which will prompt the user to
* authenticate again via BASIC authentication.
*
* <p>
* Basic authentication is an attractive protocol because it is simple and widely deployed. However, it still
* transmits a password in clear text and as such is undesirable in many situations. Digest authentication is also
* provided by Spring Security and should be used instead of Basic authentication wherever possible. See {@link
* org.springframework.security.ui.digestauth.DigestProcessingFilter}.
* <p>
* Note that if a {@link RememberMeServices} is set, this filter will automatically send back remember-me
* details to the client. Therefore, subsequent requests will not need to present a BASIC authentication header as
* they will be authenticated using the remember-me mechanism.
*
* @author Ben Alex
* @version $Id$
*/
public class BasicProcessingFilter extends SpringSecurityFilter implements InitializingBean {
//~ Instance fields ================================================================================================
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private boolean ignoreFailure = false;
private String credentialsCharset = "UTF-8";
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
if(!isIgnoreFailure()) {
Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required");
}
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (logger.isDebugEnabled()) {
logger.debug("Authorization header: " + header);
}
if ((header != null) && header.startsWith("Basic ")) {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
String token = new String(Base64.decodeBase64(base64Token), getCredentialsCharset(request));
String username = "";
String password = "";
int delim = token.indexOf(":");
if (delim != -1) {
username = token.substring(0, delim);
password = token.substring(delim + 1);
}
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
Authentication authResult;
try {
authResult = authenticationManager.authenticate(authRequest);
} catch (AuthenticationException failed) {
// Authentication failed
if (logger.isDebugEnabled()) {
logger.debug("Authentication request for user: " + username + " failed: " + failed.toString());
}
SecurityContextHolder.getContext().setAuthentication(null);
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (ignoreFailure) {
chain.doFilter(request, response);
} else {
authenticationEntryPoint.commence(request, response, failed);
}
return;
}
// Authentication success
if (logger.isDebugEnabled()) {
logger.debug("Authentication success: " + authResult.toString());
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
chain.doFilter(request, response);
}
private boolean authenticationIsRequired(String username) {
// Only reauthenticate if username doesn't match SecurityContextHolder and user isn't authenticated
// (see SEC-53)
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if(existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// Limit username comparison to providers which use usernames (ie UsernamePasswordAuthenticationToken)
// (see SEC-348)
if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
return true;
}
// Handle unusual condition where an AnonymousAuthenticationToken is already present
// This shouldn't happen very often, as BasicProcessingFitler is meant to be earlier in the filter
// chain than AnonymousProcessingFilter. Nevertheless, presence of both an AnonymousAuthenticationToken
// together with a BASIC authentication request header should indicate reauthentication using the
// BASIC protocol is desirable. This behaviour is also consistent with that provided by form and digest,
// both of which force re-authentication if the respective header is detected (and in doing so replace
// any existing AnonymousAuthenticationToken). See SEC-610.
if (existingAuth instanceof AnonymousAuthenticationToken) {
return true;
}
return false;
}
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException {
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {
}
protected AuthenticationEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected boolean isIgnoreFailure() {
return ignoreFailure;
}
public void setIgnoreFailure(boolean ignoreFailure) {
this.ignoreFailure = ignoreFailure;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
public void setCredentialsCharset(String credentialsCharset) {
Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty");
this.credentialsCharset = credentialsCharset;
}
protected String getCredentialsCharset(HttpServletRequest httpRequest) {
return credentialsCharset;
}
public int getOrder() {
return FilterChainOrder.BASIC_PROCESSING_FILTER;
}
}
@@ -0,0 +1,66 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.basicauth;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.AuthenticationException;
import org.springframework.security.ui.AuthenticationEntryPoint;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Used by the <code>SecurityEnforcementFilter</code> to commence authentication via the {@link
* BasicProcessingFilter}.<P>Once a user agent is authenticated using BASIC authentication, logout requires that
* the browser be closed or an unauthorized (401) header be sent. The simplest way of achieving the latter is to call
* the {@link #commence(HttpServletRequest, HttpServletResponse, AuthenticationException)} method below. This will indicate to
* the browser its credentials are no longer authorized, causing it to prompt the user to login again.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class BasicProcessingFilterEntryPoint implements AuthenticationEntryPoint, InitializingBean {
//~ Instance fields ================================================================================================
private String realmName;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasText(realmName, "realmName must be specified");
}
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
public String getRealmName() {
return realmName;
}
public void setRealmName(String realmName) {
this.realmName = realmName;
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Authenticates HTTP BASIC authentication requests.
</body>
</html>
@@ -0,0 +1,419 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.digestauth;
import java.io.IOException;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.security.AuthenticationException;
import org.springframework.security.AuthenticationServiceException;
import org.springframework.security.BadCredentialsException;
import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.providers.dao.UserCache;
import org.springframework.security.providers.dao.cache.NullUserCache;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.ui.WebAuthenticationDetailsSource;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.security.util.StringSplitUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Processes a HTTP request's Digest authorization headers, putting the result into the
* <code>SecurityContextHolder</code>.
* <p>
* For a detailed background on what this filter is designed to process, refer to
* <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a> (which superseded RFC 2069, although this
* filter support clients that implement either RFC 2617 or RFC 2069).
* <p>
* This filter can be used to provide Digest authentication services to both remoting protocol clients (such as
* Hessian and SOAP) as well as standard user agents (such as Internet Explorer and FireFox).
* <p>
* This Digest implementation has been designed to avoid needing to store session state between invocations.
* All session management information is stored in the "nonce" that is sent to the client by the {@link
* DigestProcessingFilterEntryPoint}.
* <p>
* If authentication is successful, the resulting {@link org.springframework.security.Authentication Authentication}
* object will be placed into the <code>SecurityContextHolder</code>.
* <p>
* If authentication fails, an {@link org.springframework.security.ui.AuthenticationEntryPoint AuthenticationEntryPoint}
* implementation is called. This must always be {@link DigestProcessingFilterEntryPoint}, which will prompt the user
* to authenticate again via Digest authentication.
* <p>
* Note there are limitations to Digest authentication, although it is a more comprehensive and secure solution
* than Basic authentication. Please see RFC 2617 section 4 for a full discussion on the advantages of Digest
* authentication over Basic authentication, including commentary on the limitations that it still imposes.
*/
public class DigestProcessingFilter extends SpringSecurityFilter implements Filter, InitializingBean, MessageSourceAware {
//~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(DigestProcessingFilter.class);
//~ Instance fields ================================================================================================
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private DigestProcessingFilterEntryPoint authenticationEntryPoint;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private UserDetailsService userDetailsService;
private boolean passwordAlreadyEncoded = false;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(userDetailsService, "A UserDetailsService is required");
Assert.notNull(authenticationEntryPoint, "A DigestProcessingFilterEntryPoint is required");
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (logger.isDebugEnabled()) {
logger.debug("Authorization header received from user agent: " + header);
}
if ((header != null) && header.startsWith("Digest ")) {
String section212response = header.substring(7);
String[] headerEntries = StringSplitUtils.splitIgnoringQuotes(section212response, ',');
Map<String,String> headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\"");
String username = (String) headerMap.get("username");
String realm = (String) headerMap.get("realm");
String nonce = (String) headerMap.get("nonce");
String uri = (String) headerMap.get("uri");
String responseDigest = (String) headerMap.get("response");
String qop = (String) headerMap.get("qop"); // RFC 2617 extension
String nc = (String) headerMap.get("nc"); // RFC 2617 extension
String cnonce = (String) headerMap.get("cnonce"); // RFC 2617 extension
// Check all required parameters were supplied (ie RFC 2069)
if ((username == null) || (realm == null) || (nonce == null) || (uri == null) || (response == null)) {
if (logger.isDebugEnabled()) {
logger.debug("extracted username: '" + username + "'; realm: '" + username + "'; nonce: '"
+ username + "'; uri: '" + username + "'; response: '" + username + "'");
}
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingMandatory",
new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
return;
}
// Check all required parameters for an "auth" qop were supplied (ie RFC 2617)
if ("auth".equals(qop)) {
if ((nc == null) || (cnonce == null)) {
if (logger.isDebugEnabled()) {
logger.debug("extracted nc: '" + nc + "'; cnonce: '" + cnonce + "'");
}
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingAuth",
new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
return;
}
}
// Check realm name equals what we expected
if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectRealm",
new Object[]{realm, this.getAuthenticationEntryPoint().getRealmName()},
"Response realm name '{0}' does not match system realm name of '{1}'")));
return;
}
// Check nonce was a Base64 encoded (as sent by DigestProcessingFilterEntryPoint)
if (!Base64.isArrayByteBase64(nonce.getBytes())) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceEncoding",
new Object[]{nonce}, "Nonce is not encoded in Base64; received nonce {0}")));
return;
}
// Decode nonce from Base64
// format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
String nonceAsPlainText = new String(Base64.decodeBase64(nonce.getBytes()));
String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":");
if (nonceTokens.length != 2) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotTwoTokens",
new Object[]{nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}")));
return;
}
// Extract expiry time from nonce
long nonceExpiryTime;
try {
nonceExpiryTime = new Long(nonceTokens[0]).longValue();
} catch (NumberFormatException nfe) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotNumeric",
new Object[]{nonceAsPlainText},
"Nonce token should have yielded a numeric first token, but was {0}")));
return;
}
// Check signature of nonce matches this expiry time
String expectedNonceSignature = DigestUtils.md5Hex(nonceExpiryTime + ":"
+ this.getAuthenticationEntryPoint().getKey());
if (!expectedNonceSignature.equals(nonceTokens[1])) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceCompromised",
new Object[]{nonceAsPlainText}, "Nonce token compromised {0}")));
return;
}
// Lookup password for presented username
// NB: DAO-provided password MUST be clear text - not encoded/salted
// (unless this instance's passwordAlreadyEncoded property is 'false')
boolean loadedFromDao = false;
UserDetails user = userCache.getUserFromCache(username);
if (user == null) {
loadedFromDao = true;
try {
user = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) {
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
new Object[]{username}, "Username {0} not found")));
return;
}
if (user == null) {
throw new AuthenticationServiceException(
"AuthenticationDao returned null, which is an interface contract violation");
}
userCache.putUserInCache(user);
}
// Compute the expected response-digest (will be in hex form)
String serverDigestMd5;
// Don't catch IllegalArgumentException (already checked validity)
serverDigestMd5 = generateDigest(passwordAlreadyEncoded, username, realm, user.getPassword(),
((HttpServletRequest) request).getMethod(), uri, qop, nonce, nc, cnonce);
// If digest is incorrect, try refreshing from backend and recomputing
if (!serverDigestMd5.equals(responseDigest) && !loadedFromDao) {
if (logger.isDebugEnabled()) {
logger.debug(
"Digest comparison failure; trying to refresh user from DAO in case password had changed");
}
try {
user = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) {
// Would very rarely happen, as user existed earlier
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
new Object[]{username}, "Username {0} not found")));
}
userCache.putUserInCache(user);
// Don't catch IllegalArgumentException (already checked validity)
serverDigestMd5 = generateDigest(passwordAlreadyEncoded, username, realm, user.getPassword(),
((HttpServletRequest) request).getMethod(), uri, qop, nonce, nc, cnonce);
}
// If digest is still incorrect, definitely reject authentication attempt
if (!serverDigestMd5.equals(responseDigest)) {
if (logger.isDebugEnabled()) {
logger.debug("Expected response: '" + serverDigestMd5 + "' but received: '" + responseDigest
+ "'; is AuthenticationDao returning clear text passwords?");
}
fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectResponse",
"Incorrect response")));
return;
}
// To get this far, the digest must have been valid
// Check the nonce has not expired
// We do this last so we can direct the user agent its nonce is stale
// but the request was otherwise appearing to be valid
if (nonceExpiryTime < System.currentTimeMillis()) {
fail(request, response,
new NonceExpiredException(messages.getMessage("DigestProcessingFilter.nonceExpired",
"Nonce has expired/timed out")));
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success for user: '" + username + "' with response: '" + responseDigest
+ "'");
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user,
user.getPassword());
authRequest.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
public static String encodePasswordInA1Format(String username, String realm, String password) {
String a1 = username + ":" + realm + ":" + password;
String a1Md5 = new String(DigestUtils.md5Hex(a1));
return a1Md5;
}
private void fail(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(null);
if (logger.isDebugEnabled()) {
logger.debug(failed);
}
authenticationEntryPoint.commence(request, response, failed);
}
/**
* Computes the <code>response</code> portion of a Digest authentication header. Both the server and user
* agent should compute the <code>response</code> independently. Provided as a static method to simplify the
* coding of user agents.
*
* @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if
* it is plain text.
* @param username the user's login name.
* @param realm the name of the realm.
* @param password the user's password in plaintext or ready-encoded.
* @param httpMethod the HTTP request method (GET, POST etc.)
* @param uri the request URI.
* @param qop the qop directive, or null if not set.
* @param nonce the nonce supplied by the server
* @param nc the "nonce-count" as defined in RFC 2617.
* @param cnonce opaque string supplied by the client when qop is set.
* @return the MD5 of the digest authentication response, encoded in hex
* @throws IllegalArgumentException if the supplied qop value is unsupported.
*/
public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
throws IllegalArgumentException {
String a1Md5 = null;
String a2 = httpMethod + ":" + uri;
String a2Md5 = new String(DigestUtils.md5Hex(a2));
if (passwordAlreadyEncoded) {
a1Md5 = password;
} else {
a1Md5 = encodePasswordInA1Format(username, realm, password);
}
String digest;
if (qop == null) {
// as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
digest = a1Md5 + ":" + nonce + ":" + a2Md5;
} else if ("auth".equals(qop)) {
// As per RFC 2617 compliant clients
digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5;
} else {
throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
}
String digestMd5 = new String(DigestUtils.md5Hex(digest));
return digestMd5;
}
public DigestProcessingFilterEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}
public UserCache getUserCache() {
return userCache;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setAuthenticationEntryPoint(DigestProcessingFilterEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setPasswordAlreadyEncoded(boolean passwordAlreadyEncoded) {
this.passwordAlreadyEncoded = passwordAlreadyEncoded;
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public int getOrder() {
return FilterChainOrder.DIGEST_PROCESSING_FILTER;
}
}
@@ -0,0 +1,130 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.digestauth;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.AuthenticationException;
import org.springframework.security.ui.AuthenticationEntryPoint;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
/**
* Used by the <code>SecurityEnforcementFilter</code> to commence authentication via the {@link
* DigestProcessingFilter}.<p>The nonce sent back to the user agent will be valid for the period indicated by
* {@link #setNonceValiditySeconds(int)}. By default this is 300 seconds. Shorter times should be used if replay
* attacks are a major concern. Larger values can be used if performance is a greater concern. This class correctly
* presents the <code>stale=true</code> header when the nonce has expierd, so properly implemented user agents will
* automatically renegotiate with a new nonce value (ie without presenting a new password dialog box to the user).</p>
*
* @author Ben Alex
* @version $Id$
*/
public class DigestProcessingFilterEntryPoint implements AuthenticationEntryPoint, InitializingBean, Ordered {
//~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(DigestProcessingFilterEntryPoint.class);
//~ Instance fields ================================================================================================
private String key;
private String realmName;
private int nonceValiditySeconds = 300;
private int order = Integer.MAX_VALUE; // ~ default
//~ Methods ========================================================================================================
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public void afterPropertiesSet() throws Exception {
if ((realmName == null) || "".equals(realmName)) {
throw new IllegalArgumentException("realmName must be specified");
}
if ((key == null) || "".equals(key)) {
throw new IllegalArgumentException("key must be specified");
}
}
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// compute a nonce (do not use remote IP address due to proxy farms)
// format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
long expiryTime = System.currentTimeMillis() + (nonceValiditySeconds * 1000);
String signatureValue = new String(DigestUtils.md5Hex(expiryTime + ":" + key));
String nonceValue = expiryTime + ":" + signatureValue;
String nonceValueBase64 = new String(Base64.encodeBase64(nonceValue.getBytes()));
// qop is quality of protection, as defined by RFC 2617.
// we do not use opaque due to IE violation of RFC 2617 in not
// representing opaque on subsequent requests in same session.
String authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"auth\", nonce=\""
+ nonceValueBase64 + "\"";
if (authException instanceof NonceExpiredException) {
authenticateHeader = authenticateHeader + ", stale=\"true\"";
}
if (logger.isDebugEnabled()) {
logger.debug("WWW-Authenticate header sent to user agent: " + authenticateHeader);
}
httpResponse.addHeader("WWW-Authenticate", authenticateHeader);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
public String getKey() {
return key;
}
public int getNonceValiditySeconds() {
return nonceValiditySeconds;
}
public String getRealmName() {
return realmName;
}
public void setKey(String key) {
this.key = key;
}
public void setNonceValiditySeconds(int nonceValiditySeconds) {
this.nonceValiditySeconds = nonceValiditySeconds;
}
public void setRealmName(String realmName) {
this.realmName = realmName;
}
}
@@ -0,0 +1,50 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.digestauth;
import org.springframework.security.AuthenticationException;
/**
* Thrown if an authentication request is rejected because the digest nonce has expired.
*
* @author Ben Alex
* @version $Id$
*/
public class NonceExpiredException extends AuthenticationException {
//~ Constructors ===================================================================================================
/**
* Constructs a <code>NonceExpiredException</code> with the specified
* message.
*
* @param msg the detail message
*/
public NonceExpiredException(String msg) {
super(msg);
}
/**
* Constructs a <code>NonceExpiredException</code> with the specified
* message and root cause.
*
* @param msg the detail message
* @param t root cause
*/
public NonceExpiredException(String msg, Throwable t) {
super(msg, t);
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Authenticates HTTP Digest authentication requests.
</body>
</html>
@@ -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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.logout;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.Authentication;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.LogoutSuccessHandler;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Logs a principal out.
* <p>
* Polls a series of {@link LogoutHandler}s. The handlers should be specified in the order they are required.
* Generally you will want to call logout handlers <code>TokenBasedRememberMeServices</code> and
* <code>SecurityContextLogoutHandler</code> (in that order).
* <p>
* After logout, a redirect will be performed to the URL determined by either the configured
* <tt>LogoutSuccessHandler</tt> or the <tt>logoutSuccessUrl</tt>, depending on which constructor was used.
*
* @author Ben Alex
* @version $Id$
*/
public class LogoutFilter extends SpringSecurityFilter {
//~ Instance fields ================================================================================================
private String filterProcessesUrl = "/j_spring_security_logout";
private List<LogoutHandler> handlers; private LogoutSuccessHandler logoutSuccessHandler;
//~ Constructors ===================================================================================================
/**
* Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the target destination
* after logging out. The list of <tt>LogoutHandler</tt>s are intended to perform the actual logout functionality
* (such as clearing the security context, invalidating the session, etc.).
*/
public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
Assert.notEmpty(handlers, "LogoutHandlers are required");
this.handlers = Arrays.asList(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
}
public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
Assert.notEmpty(handlers, "LogoutHandlers are required");
this.handlers = Arrays.asList(handlers);
Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) ||
UrlUtils.isValidRedirectUrl(logoutSuccessUrl), logoutSuccessUrl + " isn't a valid redirect URL");
SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
if (StringUtils.hasText(logoutSuccessUrl)) {
urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
}
logoutSuccessHandler = urlLogoutSuccessHandler;
}
//~ Methods ========================================================================================================
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth + "' and transferring to logout destination");
}
for (LogoutHandler handler : handlers) {
handler.logout(request, response, auth);
}
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
/**
* Allow subclasses to modify when a logout should take place.
*
* @param request the request
* @param response the response
*
* @return <code>true</code> if logout should occur, <code>false</code> otherwise
*/
protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything from the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
int queryParamIndex = uri.indexOf('?');
if (queryParamIndex > 0) {
// strip everything from the first question mark
uri = uri.substring(0, queryParamIndex);
}
if ("".equals(request.getContextPath())) {
return uri.endsWith(filterProcessesUrl);
}
return uri.endsWith(request.getContextPath() + filterProcessesUrl);
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid value for" +
" 'filterProcessesUrl'");
this.filterProcessesUrl = filterProcessesUrl;
}
protected String getFilterProcessesUrl() {
return filterProcessesUrl;
}
public int getOrder() {
return FilterChainOrder.LOGOUT_FILTER;
}
}
@@ -0,0 +1,44 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.logout;
import org.springframework.security.Authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Indicates a class that is able to participate in logout handling.
*
* <p>
* Called by {@link LogoutFilter}.
*
* @author Ben Alex
* @version $Id$
*/
public interface LogoutHandler {
//~ Methods ========================================================================================================
/**
* Causes a logout to be completed. The method must complete successfully.
*
* @param request the HTTP request
* @param response the HTTP response
* @param authentication the current principal details
*/
void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication);
}
@@ -0,0 +1,74 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.logout;
import org.springframework.security.Authentication;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Performs a logout by modifying the {@link org.springframework.security.context.SecurityContextHolder}.
* <p>
* Will also invalidate the {@link HttpSession} if {@link #isInvalidateHttpSession()} is <code>true</code> and the
* session is not <code>null</code>.
*
* @author Ben Alex
* @version $Id$
*/
public class SecurityContextLogoutHandler implements LogoutHandler {
private boolean invalidateHttpSession = true;
//~ Methods ========================================================================================================
/**
* Requires the request to be passed in.
*
* @param request from which to obtain a HTTP session (cannot be null)
* @param response not used (can be <code>null</code>)
* @param authentication not used (can be <code>null</code>)
*/
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
SecurityContextHolder.clearContext();
}
public boolean isInvalidateHttpSession() {
return invalidateHttpSession;
}
/**
* Causes the {@link HttpSession} to be invalidated when this {@link LogoutHandler} is invoked. Defaults to true.
*
* @param invalidateHttpSession true if you wish the session to be invalidated (default) or false if it should
* not be.
*/
public void setInvalidateHttpSession(boolean invalidateHttpSession) {
this.invalidateHttpSession = invalidateHttpSession;
}
}
@@ -0,0 +1,29 @@
package org.springframework.security.ui.logout;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.Authentication;
import org.springframework.security.ui.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.ui.LogoutSuccessHandler;
/**
* Handles the navigation on logout by delegating to the {@link AbstractAuthenticationTargetUrlRequestHandler}
* base class logic.
*
* @author Luke Taylor
* @version $Id$
* @since 2.5
*/
public class SimpleUrlLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler
implements LogoutSuccessHandler {
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
super.handle(request, response, authentication);
}
}
@@ -0,0 +1,6 @@
<html>
<body>
Authentication processing mechanisms, which respond to the submission of authentication
credentials using various protocols (eg BASIC, CAS, form login etc).
</body>
</html>
@@ -0,0 +1,174 @@
package org.springframework.security.ui.preauth;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;
import org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.security.ui.WebAuthenticationDetailsSource;
import org.springframework.security.ui.AbstractProcessingFilter;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
/**
* Base class for processing filters that handle pre-authenticated authentication requests. Subclasses must implement
* the getPreAuthenticatedPrincipal() and getPreAuthenticatedCredentials() methods.
* <p>
* By default, the filter chain will proceed when an authentication attempt fails in order to allow other
* authentication mechanisms to process the request. To reject the credentials immediately, set the
* <tt>continueFilterChainOnUnsuccessfulAuthentication</tt> flag to false. The exception raised by the
* <tt>AuthenticationManager</tt> will the be re-thrown. Note that this will not affect cases where the principal
* returned by {@link #getPreAuthenticatedPrincipal} is null, when the chain will still proceed as normal.
*
*
* @author Luke Taylor
* @author Ruud Senden
* @since 2.0
*/
public abstract class AbstractPreAuthenticatedProcessingFilter extends SpringSecurityFilter implements
InitializingBean, ApplicationEventPublisherAware {
private ApplicationEventPublisher eventPublisher = null;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager = null;
private boolean continueFilterChainOnUnsuccessfulAuthentication = true;
/**
* Check whether all required properties have been set.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(authenticationManager, "An AuthenticationManager must be set");
}
/**
* Try to authenticate a pre-authenticated user with Spring Security if the user has not yet been authenticated.
*/
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Checking secure context token: " + SecurityContextHolder.getContext().getAuthentication());
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
doAuthenticate(request, response);
}
filterChain.doFilter(request, response);
}
/**
* Do the actual authentication for a pre-authenticated user.
*/
private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) {
Authentication authResult = null;
Object principal = getPreAuthenticatedPrincipal(request);
Object credentials = getPreAuthenticatedCredentials(request);
if (principal == null) {
if (logger.isDebugEnabled()) {
logger.debug("No pre-authenticated principal found in request");
}
return;
}
if (logger.isDebugEnabled()) {
logger.debug("preAuthenticatedPrincipal = " + principal + ", trying to authenticate");
}
try {
PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(principal, credentials);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
authResult = authenticationManager.authenticate(authRequest);
successfulAuthentication(request, response, authResult);
} catch (AuthenticationException failed) {
unsuccessfulAuthentication(request, response, failed);
if (!continueFilterChainOnUnsuccessfulAuthentication) {
throw failed;
}
}
}
/**
* Puts the <code>Authentication</code> instance returned by the
* authentication manager into the secure context.
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
}
/**
* Ensures the authentication object in the secure context is set to null
* when authentication fails.
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Cleared security context due to exception", failed);
}
request.getSession().setAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY, failed);
}
/**
* @param anApplicationEventPublisher
* The ApplicationEventPublisher to use
*/
public void setApplicationEventPublisher(ApplicationEventPublisher anApplicationEventPublisher) {
this.eventPublisher = anApplicationEventPublisher;
}
/**
* @param authenticationDetailsSource
* The AuthenticationDetailsSource to use
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* @param authenticationManager
* The AuthenticationManager to use
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setContinueFilterChainOnUnsuccessfulAuthentication(boolean shouldContinue) {
continueFilterChainOnUnsuccessfulAuthentication = shouldContinue;
}
/**
* Override to extract the principal information from the current request
*/
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
/**
* Override to extract the credentials (if applicable) from the current request. Some implementations
* may return a dummy value.
*/
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
}
@@ -0,0 +1,11 @@
package org.springframework.security.ui.preauth;
import org.springframework.security.AuthenticationException;
public class PreAuthenticatedCredentialsNotFoundException extends AuthenticationException {
public PreAuthenticatedCredentialsNotFoundException(String msg) {
super(msg);
}
}
@@ -0,0 +1,54 @@
package org.springframework.security.ui.preauth;
import java.util.Collections;
import java.util.List;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.MutableGrantedAuthoritiesContainer;
import org.springframework.security.ui.AuthenticationDetails;
import org.springframework.util.Assert;
/**
* This AuthenticationDetails implementation allows for storing a list of
* pre-authenticated Granted Authorities.
*
* @author Ruud Senden
* @since 2.0
*/
public class PreAuthenticatedGrantedAuthoritiesAuthenticationDetails extends AuthenticationDetails implements
MutableGrantedAuthoritiesContainer {
public static final long serialVersionUID = 1L;
private List<GrantedAuthority> preAuthenticatedGrantedAuthorities = null;
public PreAuthenticatedGrantedAuthoritiesAuthenticationDetails(Object context) {
super(context);
}
/**
* @return The String representation of this object.
*/
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString() + "; ");
sb.append("preAuthenticatedGrantedAuthorities: " + preAuthenticatedGrantedAuthorities);
return sb.toString();
}
/**
*
* @see org.springframework.security.GrantedAuthoritiesContainer#getGrantedAuthorities()
*/
public List<GrantedAuthority> getGrantedAuthorities() {
Assert.notNull(preAuthenticatedGrantedAuthorities, "Pre-authenticated granted authorities have not been set");
return preAuthenticatedGrantedAuthorities;
}
/**
* @see org.springframework.security.MutableGrantedAuthoritiesContainer#setGrantedAuthorities()
*/
public void setGrantedAuthorities(List<GrantedAuthority> aJ2eeBasedGrantedAuthorities) {
this.preAuthenticatedGrantedAuthorities = Collections.unmodifiableList(aJ2eeBasedGrantedAuthorities);
}
}
@@ -0,0 +1,44 @@
package org.springframework.security.ui.preauth;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.ui.WebAuthenticationDetails;
import org.springframework.security.GrantedAuthoritiesContainerImpl;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.MutableGrantedAuthoritiesContainer;
/**
* This WebAuthenticationDetails implementation allows for storing a list of
* pre-authenticated Granted Authorities.
*
* @author Ruud Senden
* @author Luke Taylor
* @since 2.0
*/
public class PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails extends WebAuthenticationDetails implements
MutableGrantedAuthoritiesContainer {
public static final long serialVersionUID = 1L;
private MutableGrantedAuthoritiesContainer authoritiesContainer = new GrantedAuthoritiesContainerImpl();
public PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(HttpServletRequest request) {
super(request);
}
public List<GrantedAuthority> getGrantedAuthorities() {
return authoritiesContainer.getGrantedAuthorities();
}
public void setGrantedAuthorities(List<GrantedAuthority> authorities) {
this.authoritiesContainer.setGrantedAuthorities(authorities);
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(super.toString() + "; ");
sb.append(authoritiesContainer);
return sb.toString();
}
}
@@ -0,0 +1,65 @@
package org.springframework.security.ui.preauth;
import org.springframework.security.AuthenticationException;
import org.springframework.security.ui.AuthenticationEntryPoint;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
/**
* <p>
* In the pre-authenticated authentication case (unlike CAS, for example) the
* user will already have been identified through some external mechanism and a
* secure context established by the time the security-enforcement filter is
* invoked.
* <p>
* Therefore this class isn't actually responsible for the commencement of
* authentication, as it is in the case of other providers. It will be called if
* the user is rejected by the AbstractPreAuthenticatedProcessingFilter,
* resulting in a null authentication.
* <p>
* The <code>commence</code> method will always return an
* <code>HttpServletResponse.SC_FORBIDDEN</code> (403 error).
* <p>
* This code is based on
* {@link org.springframework.security.ui.x509.X509ProcessingFilterEntryPoint}.
*
* @see org.springframework.security.ui.ExceptionTranslationFilter
*
* @author Luke Taylor
* @author Ruud Senden
* @since 2.0
*/
public class PreAuthenticatedProcessingFilterEntryPoint implements AuthenticationEntryPoint, Ordered {
private static final Log logger = LogFactory.getLog(PreAuthenticatedProcessingFilterEntryPoint.class);
private int order = Integer.MAX_VALUE;
/**
* Always returns a 403 error code to the client.
*/
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException,
ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Pre-authenticated entry point called. Rejecting access");
}
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
}
public int getOrder() {
return order;
}
public void setOrder(int i) {
order = i;
}
}
@@ -0,0 +1,76 @@
package org.springframework.security.ui.preauth.header;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.ui.preauth.PreAuthenticatedCredentialsNotFoundException;
import org.springframework.util.Assert;
/**
* A simple pre-authenticated filter which obtains the username from a request header, for use with systems such as
* CA Siteminder.
* <p>
* As with most pre-authenticated scenarios, it is essential that the external authentication system is set up
* correctly as this filter does no authentication whatsoever. All the protection is assumed to be provided externally
* and if this filter is included inappropriately in a configuration, it would be possible to assume the
* identity of a user merely by setting the correct header name. This also means it should not be used in combination
* with other Spring Security authentication mechanisms such as form login, as this would imply there was a means of
* bypassing the external system which would be risky.
* <p>
* The property <tt>principalRequestHeader</tt> is the name of the request header that contains the username. It
* defaults to "SM_USER" for compatibility with Siteminder.
*
*
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public class RequestHeaderPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
private String principalRequestHeader = "SM_USER";
private String credentialsRequestHeader;
/**
* Read and returns the header named by <tt>principalRequestHeader</tt> from the request.
*
* @throws PreAuthenticatedCredentialsNotFoundException if the header is missing
*/
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String principal = request.getHeader(principalRequestHeader);
if (principal == null) {
throw new PreAuthenticatedCredentialsNotFoundException(principalRequestHeader
+ " header not found in request.");
}
return principal;
}
/**
* Credentials aren't usually applicable, but if a <tt>credentialsRequestHeader</tt> is set, this
* will be read and used as the credentials value. Otherwise a dummy value will be used.
*/
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (credentialsRequestHeader != null) {
String credentials = request.getHeader(credentialsRequestHeader);
return credentials;
}
return "N/A";
}
public void setPrincipalRequestHeader(String principalRequestHeader) {
Assert.hasText(principalRequestHeader, "principalRequestHeader must not be empty or null");
this.principalRequestHeader = principalRequestHeader;
}
public void setCredentialsRequestHeader(String credentialsRequestHeader) {
Assert.hasText(credentialsRequestHeader, "credentialsRequestHeader must not be empty or null");
this.credentialsRequestHeader = credentialsRequestHeader;
}
public int getOrder() {
return FilterChainOrder.PRE_AUTH_FILTER;
}
}
@@ -0,0 +1,92 @@
package org.springframework.security.ui.preauth.j2ee;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.MutableGrantedAuthoritiesContainer;
import org.springframework.security.authoritymapping.Attributes2GrantedAuthoritiesMapper;
import org.springframework.security.authoritymapping.MappableAttributesRetriever;
import org.springframework.security.authoritymapping.SimpleAttributes2GrantedAuthoritiesMapper;
import org.springframework.security.ui.AuthenticationDetailsSourceImpl;
import org.springframework.util.Assert;
/**
* Base implementation for classes scenarios where the authentication details object is used
* to store a list of authorities obtained from the context object (such as an HttpServletRequest)
* passed to {@link #buildDetails(Object)}.
* <p>
*
*
* @author Luke Taylor
* @since 2.0
*/
public abstract class AbstractPreAuthenticatedAuthenticationDetailsSource extends AuthenticationDetailsSourceImpl {
protected final Log logger = LogFactory.getLog(getClass());
protected Set<String> j2eeMappableRoles;
protected Attributes2GrantedAuthoritiesMapper j2eeUserRoles2GrantedAuthoritiesMapper =
new SimpleAttributes2GrantedAuthoritiesMapper();
public AbstractPreAuthenticatedAuthenticationDetailsSource() {
}
/**
* Check that all required properties have been set.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(j2eeMappableRoles, "No mappable roles available");
Assert.notNull(j2eeUserRoles2GrantedAuthoritiesMapper, "Roles to granted authorities mapper not set");
}
/**
* Build the authentication details object. If the specified authentication
* details class implements {@link MutableGrantedAuthoritiesContainer}, a
* list of pre-authenticated Granted Authorities will be set based on the
* roles for the current user.
*
* @see org.springframework.security.ui.AuthenticationDetailsSource#buildDetails(Object)
*/
public Object buildDetails(Object context) {
Object result = super.buildDetails(context);
if (result instanceof MutableGrantedAuthoritiesContainer) {
Collection<String> j2eeUserRoles = getUserRoles(context, j2eeMappableRoles);
List<GrantedAuthority> userGas = j2eeUserRoles2GrantedAuthoritiesMapper.getGrantedAuthorities(j2eeUserRoles);
if (logger.isDebugEnabled()) {
logger.debug("J2EE roles [" + j2eeUserRoles + "] mapped to Granted Authorities: [" + userGas + "]");
}
((MutableGrantedAuthoritiesContainer) result).setGrantedAuthorities(userGas);
}
return result;
}
/**
* Allows the roles of the current user to be determined from the context object
*
* @param context the context object (an HttpRequest, PortletRequest etc)
* @param mappableRoles the possible roles as determined by the MappableAttributesRetriever
* @return the subset of mappable roles which the current user has.
*/
protected abstract Collection<String> getUserRoles(Object context, Set<String> mappableRoles);
/**
* @param aJ2eeMappableRolesRetriever
* The MappableAttributesRetriever to use
*/
public void setMappableRolesRetriever(MappableAttributesRetriever aJ2eeMappableRolesRetriever) {
this.j2eeMappableRoles = aJ2eeMappableRolesRetriever.getMappableAttributes();
}
/**
* @param mapper
* The Attributes2GrantedAuthoritiesMapper to use
*/
public void setUserRoles2GrantedAuthoritiesMapper(Attributes2GrantedAuthoritiesMapper mapper) {
j2eeUserRoles2GrantedAuthoritiesMapper = mapper;
}
}
@@ -0,0 +1,49 @@
package org.springframework.security.ui.preauth.j2ee;
import org.springframework.security.ui.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.authoritymapping.SimpleAttributes2GrantedAuthoritiesMapper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
/**
* Implementation of AuthenticationDetailsSource which converts the user's J2EE roles (as obtained by calling
* {@link HttpServletRequest#isUserInRole(String)}) into GrantedAuthoritys and stores these in the authentication
* details object (.
*
* @author Ruud Senden
* @since 2.0
*/
public class J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource extends AbstractPreAuthenticatedAuthenticationDetailsSource {
/**
* Public constructor which overrides the default AuthenticationDetails
* class to be used.
*/
public J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource() {
super.setClazz(PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails.class);
j2eeUserRoles2GrantedAuthoritiesMapper = new SimpleAttributes2GrantedAuthoritiesMapper();
}
/**
* Obtains the list of user roles based on the current user's J2EE roles.
*
* @param request The request against which <tt>isUserInRole</tt> will be called for each role name
* returned by the MappableAttributesRetriever.
* @return GrantedAuthority[] mapped from the user's J2EE roles.
*/
protected Collection<String> getUserRoles(Object context, Set<String> mappableRoles) {
ArrayList<String> j2eeUserRolesList = new ArrayList<String>();
for (String role : mappableRoles) {
if (((HttpServletRequest)context).isUserInRole(role)) {
j2eeUserRolesList.add(role);
}
}
return j2eeUserRolesList;
}
}
@@ -0,0 +1,39 @@
package org.springframework.security.ui.preauth.j2ee;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter;
/**
* This AbstractPreAuthenticatedProcessingFilter implementation is based on the
* J2EE container-based authentication mechanism. It will use the J2EE user
* principal name as the pre-authenticated principal.
*
* @author Ruud Senden
* @since 2.0
*/
public class J2eePreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Return the J2EE user name.
*/
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpRequest) {
Object principal = httpRequest.getUserPrincipal() == null ? null : httpRequest.getUserPrincipal().getName();
if (logger.isDebugEnabled()) {
logger.debug("PreAuthenticated J2EE principal: " + principal);
}
return principal;
}
/**
* For J2EE container-based authentication there is no generic way to
* retrieve the credentials, as such this method returns a fixed dummy
* value.
*/
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpRequest) {
return "N/A";
}
public int getOrder() {
return 0;
}
}
@@ -0,0 +1,50 @@
package org.springframework.security.ui.preauth.j2ee;
import java.io.InputStream;
import org.springframework.security.authoritymapping.XmlMappableAttributesRetriever;
/**
* <p>
* This MappableAttributesRetriever implementation reads the list of defined J2EE
* roles from a web.xml file. It's functionality is based on the
* XmlMappableAttributesRetriever base class.
* <p>
* Example on how to configure this MappableAttributesRetriever in the Spring
* configuration file:
*
* <pre>
*
*
* &lt;bean id=&quot;j2eeMappableRolesRetriever&quot; class=&quot;org.springframework.security.ui.preauth.j2ee.WebXmlMappableAttributesRetriever&quot;&gt;
* &lt;property name=&quot;webXmlInputStream&quot;&gt;&lt;bean factory-bean=&quot;webXmlResource&quot; factory-method=&quot;getInputStream&quot;/&gt;&lt;/property&gt;
* &lt;/bean&gt;
* &lt;bean id=&quot;webXmlResource&quot; class=&quot;org.springframework.web.context.support.ServletContextResource&quot;&gt;
* &lt;constructor-arg&gt;&lt;ref local=&quot;servletContext&quot;/&gt;&lt;/constructor-arg&gt;
* &lt;constructor-arg&gt;&lt;value&gt;/WEB-INF/web.xml&lt;/value&gt;&lt;/constructor-arg&gt;
* &lt;/bean&gt;
* &lt;bean id=&quot;servletContext&quot; class=&quot;org.springframework.web.context.support.ServletContextFactoryBean&quot;/&gt;
*
* </pre>
*
* @author Ruud Senden
* @since 2.0
*/
public class WebXmlMappableAttributesRetriever extends XmlMappableAttributesRetriever {
private static final String XPATH_EXPR = "/web-app/security-role/role-name/text()";
/**
* Constructor setting the XPath expression to use
*/
public WebXmlMappableAttributesRetriever() {
super.setXpathExpression(XPATH_EXPR);
}
/**
* @param anInputStream
* The InputStream to read the XML data from
*/
public void setWebXmlInputStream(InputStream anInputStream) {
super.setXmlInputStream(anInputStream);
}
}
@@ -0,0 +1,206 @@
package org.springframework.security.ui.preauth.websphere;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import javax.security.auth.Subject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* WebSphere Security helper class to allow retrieval of the current username and groups.
* <p>
* See Spring Security Jira SEC-477.
*
* @author Ruud Senden
* @author Stephane Manciot
* @since 2.0
*/
final class WASSecurityHelper {
private static final Log logger = LogFactory.getLog(WASSecurityHelper.class);
private static final String USER_REGISTRY = "UserRegistry";
private static Method getRunAsSubject = null;
private static Method getGroupsForUser = null;
private static Method getSecurityName = null;
// SEC-803
private static Class<?> wsCredentialClass = null;
/**
* Get the security name for the given subject.
*
* @param subject
* The subject for which to retrieve the security name
* @return String the security name for the given subject
*/
private static final String getSecurityName(final Subject subject) {
if (logger.isDebugEnabled()) {
logger.debug("Determining Websphere security name for subject " + subject);
}
String userSecurityName = null;
if (subject != null) {
// SEC-803
Object credential = subject.getPublicCredentials(getWSCredentialClass()).iterator().next();
if (credential != null) {
userSecurityName = (String)invokeMethod(getSecurityNameMethod(),credential,null);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Websphere security name is " + userSecurityName + " for subject " + subject);
}
return userSecurityName;
}
/**
* Get the current RunAs subject.
*
* @return Subject the current RunAs subject
*/
private static final Subject getRunAsSubject() {
logger.debug("Retrieving WebSphere RunAs subject");
// get Subject: WSSubject.getCallerSubject ();
return (Subject) invokeMethod(getRunAsSubjectMethod(), null, new Object[] {});
}
/**
* Get the WebSphere group names for the given subject.
*
* @param subject
* The subject for which to retrieve the WebSphere group names
* @return the WebSphere group names for the given subject
*/
private static final String[] getWebSphereGroups(final Subject subject) {
return getWebSphereGroups(getSecurityName(subject));
}
/**
* Get the WebSphere group names for the given security name.
*
* @param securityName
* The securityname for which to retrieve the WebSphere group names
* @return the WebSphere group names for the given security name
*/
@SuppressWarnings("unchecked")
private static final String[] getWebSphereGroups(final String securityName) {
Context ic = null;
try {
// TODO: Cache UserRegistry object
ic = new InitialContext();
Object objRef = ic.lookup(USER_REGISTRY);
Object userReg = PortableRemoteObject.narrow(objRef, Class.forName ("com.ibm.websphere.security.UserRegistry"));
if (logger.isDebugEnabled()) {
logger.debug("Determining WebSphere groups for user " + securityName + " using WebSphere UserRegistry " + userReg);
}
final Collection groups = (Collection) invokeMethod(getGroupsForUserMethod(), userReg, new Object[]{ securityName });
if (logger.isDebugEnabled()) {
logger.debug("Groups for user " + securityName + ": " + groups.toString());
}
String[] result = new String[groups.size()];
return (String[]) groups.toArray(result);
} catch (Exception e) {
logger.error("Exception occured while looking up groups for user", e);
throw new RuntimeException("Exception occured while looking up groups for user", e);
} finally {
try {
ic.close();
} catch (NamingException e) {
logger.debug("Exception occured while closing context", e);
}
}
}
/**
* @return
*/
public static final String[] getGroupsForCurrentUser() {
return getWebSphereGroups(getRunAsSubject());
}
public static final String getCurrentUserName() {
return getSecurityName(getRunAsSubject());
}
private static final Object invokeMethod(Method method, Object instance, Object[] args)
{
try {
return method.invoke(instance,args);
} catch (IllegalArgumentException e) {
logger.error("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+ Arrays.asList(args)+")",e);
throw new RuntimeException("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+Arrays.asList(args)+")",e);
} catch (IllegalAccessException e) {
logger.error("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+Arrays.asList(args)+")",e);
throw new RuntimeException("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+Arrays.asList(args)+")",e);
} catch (InvocationTargetException e) {
logger.error("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+Arrays.asList(args)+")",e);
throw new RuntimeException("Error while invoking method "+method.getClass().getName()+"."+method.getName()+"("+Arrays.asList(args)+")",e);
}
}
private static final Method getMethod(String className, String methodName, String[] parameterTypeNames) {
try {
Class<?> c = Class.forName(className);
final int len = parameterTypeNames.length;
Class<?>[] parameterTypes = new Class[len];
for (int i = 0; i < len; i++) {
parameterTypes[i] = Class.forName(parameterTypeNames[i]);
}
return c.getDeclaredMethod(methodName, parameterTypes);
} catch (ClassNotFoundException e) {
logger.error("Required class"+className+" not found");
throw new RuntimeException("Required class"+className+" not found",e);
} catch (NoSuchMethodException e) {
logger.error("Required method "+methodName+" with parameter types ("+ Arrays.asList(parameterTypeNames) +") not found on class "+className);
throw new RuntimeException("Required class"+className+" not found",e);
}
}
private static final Method getRunAsSubjectMethod() {
if (getRunAsSubject == null) {
getRunAsSubject = getMethod("com.ibm.websphere.security.auth.WSSubject", "getRunAsSubject", new String[] {});
}
return getRunAsSubject;
}
private static final Method getGroupsForUserMethod() {
if (getGroupsForUser == null) {
getGroupsForUser = getMethod("com.ibm.websphere.security.UserRegistry", "getGroupsForUser", new String[] { "java.lang.String" });
}
return getGroupsForUser;
}
private static final Method getSecurityNameMethod() {
if (getSecurityName == null) {
getSecurityName = getMethod("com.ibm.websphere.security.cred.WSCredential", "getSecurityName", new String[] {});
}
return getSecurityName;
}
// SEC-803
private static final Class<?> getWSCredentialClass() {
if (wsCredentialClass == null) {
wsCredentialClass = getClass("com.ibm.websphere.security.cred.WSCredential");
}
return wsCredentialClass;
}
private static final Class<?> getClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
logger.error("Required class " + className + " not found");
throw new RuntimeException("Required class " + className + " not found",e);
}
}
}
@@ -0,0 +1,96 @@
package org.springframework.security.ui.preauth.websphere;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.util.Assert;
/**
* This method interceptor can be used in front of arbitrary Spring beans to make a Spring SecurityContext
* available to the bean, based on the current WebSphere credentials.
*
* @author Ruud Senden
* @since 1.0
*/
public class WebSphere2SpringSecurityPropagationInterceptor implements MethodInterceptor {
private static final Log LOG = LogFactory.getLog(WebSphere2SpringSecurityPropagationInterceptor.class);
private AuthenticationManager authenticationManager = null;
private AuthenticationDetailsSource authenticationDetailsSource = new WebSpherePreAuthenticatedAuthenticationDetailsSource();
/**
* Authenticate with Spring Security based on WebSphere credentials before proceeding with method
* invocation, and clean up the Spring Security Context after method invocation finishes.
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
try {
LOG.debug("Performing Spring Security authentication with WebSphere credentials");
authenticateSpringSecurityWithWASCredentials(this);
LOG.debug("Proceeding with method invocation");
return methodInvocation.proceed();
} finally {
LOG.debug("Clearing Spring Security security context");
clearSpringSecurityContext();
}
}
/**
* Retrieve the current WebSphere credentials and authenticate them with Spring Security
* using the pre-authenticated authentication provider.
* @param aContext The context to use for building the authentication details.
*/
private final void authenticateSpringSecurityWithWASCredentials(Object aContext)
{
Assert.notNull(authenticationManager);
Assert.notNull(authenticationDetailsSource);
String userName = WASSecurityHelper.getCurrentUserName();
if (LOG.isDebugEnabled()) { LOG.debug("Creating authentication request for user "+userName); }
PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken(userName,null);
authRequest.setDetails(authenticationDetailsSource.buildDetails(null));
if (LOG.isDebugEnabled()) { LOG.debug("Authentication request for user "+userName+": "+authRequest); }
Authentication authResponse = authenticationManager.authenticate(authRequest);
if (LOG.isDebugEnabled()) { LOG.debug("Authentication response for user "+userName+": "+authResponse); }
SecurityContextHolder.getContext().setAuthentication(authResponse);
}
/**
* Clear the Spring Security Context
*/
private final void clearSpringSecurityContext()
{
SecurityContextHolder.clearContext();
}
/**
* @return Returns the authenticationManager.
*/
public AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
/**
* @param authenticationManager The authenticationManager to set.
*/
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* @return Returns the authenticationDetailsSource.
*/
public AuthenticationDetailsSource getAuthenticationDetailsSource() {
return authenticationDetailsSource;
}
/**
* @param authenticationDetailsSource The authenticationDetailsSource to set.
*/
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
this.authenticationDetailsSource = authenticationDetailsSource;
}
}
@@ -0,0 +1,86 @@
package org.springframework.security.ui.preauth.websphere;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.MutableGrantedAuthoritiesContainer;
import org.springframework.security.authoritymapping.Attributes2GrantedAuthoritiesMapper;
import org.springframework.security.authoritymapping.SimpleAttributes2GrantedAuthoritiesMapper;
import org.springframework.security.ui.AuthenticationDetailsSourceImpl;
import org.springframework.security.ui.preauth.PreAuthenticatedGrantedAuthoritiesAuthenticationDetails;
import org.springframework.util.Assert;
/**
* This AuthenticationDetailsSource implementation, when configured with a MutableGrantedAuthoritiesContainer,
* will set the pre-authenticated granted authorities based on the WebSphere groups for the current WebSphere
* user, mapped using the configured Attributes2GrantedAuthoritiesMapper.
*
* By default, this class is configured to build instances of the
* PreAuthenticatedGrantedAuthoritiesAuthenticationDetails class.
*
* @author Ruud Senden
*/
public class WebSpherePreAuthenticatedAuthenticationDetailsSource extends AuthenticationDetailsSourceImpl implements InitializingBean {
private final Log logger = LogFactory.getLog(getClass());
private Attributes2GrantedAuthoritiesMapper webSphereGroups2GrantedAuthoritiesMapper = new SimpleAttributes2GrantedAuthoritiesMapper();
/**
* Public constructor which overrides the default AuthenticationDetails
* class to be used.
*/
public WebSpherePreAuthenticatedAuthenticationDetailsSource() {
super.setClazz(PreAuthenticatedGrantedAuthoritiesAuthenticationDetails.class);
}
/**
* Check that all required properties have been set.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(webSphereGroups2GrantedAuthoritiesMapper, "WebSphere groups to granted authorities mapper not set");
}
/**
* Build the authentication details object. If the specified authentication
* details class implements the PreAuthenticatedGrantedAuthoritiesSetter, a
* list of pre-authenticated Granted Authorities will be set based on the
* WebSphere groups for the current user.
*
* @see org.springframework.security.ui.AuthenticationDetailsSource#buildDetails(Object)
*/
public Object buildDetails(Object context) {
Object result = super.buildDetails(context);
if (result instanceof MutableGrantedAuthoritiesContainer) {
((MutableGrantedAuthoritiesContainer) result)
.setGrantedAuthorities(getWebSphereGroupsBasedGrantedAuthorities());
}
return result;
}
/**
* Get a list of Granted Authorities based on the current user's WebSphere groups.
*
* @return GrantedAuthority[] mapped from the user's WebSphere groups.
*/
private List<GrantedAuthority> getWebSphereGroupsBasedGrantedAuthorities() {
List<String> webSphereGroups = Arrays.asList(WASSecurityHelper.getGroupsForCurrentUser());
List<GrantedAuthority> userGas = webSphereGroups2GrantedAuthoritiesMapper.getGrantedAuthorities(webSphereGroups);
if (logger.isDebugEnabled()) {
logger.debug("WebSphere groups: " + webSphereGroups + " mapped to Granted Authorities: " + userGas);
}
return userGas;
}
/**
* @param mapper
* The Attributes2GrantedAuthoritiesMapper to use
*/
public void setWebSphereGroups2GrantedAuthoritiesMapper(Attributes2GrantedAuthoritiesMapper mapper) {
webSphereGroups2GrantedAuthoritiesMapper = mapper;
}
}
@@ -0,0 +1,39 @@
package org.springframework.security.ui.preauth.websphere;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter;
/**
* This AbstractPreAuthenticatedProcessingFilter implementation is based on
* WebSphere authentication. It will use the WebSphere RunAs user principal name
* as the pre-authenticated principal.
*
* @author Ruud Senden
* @since 2.0
*/
public class WebSpherePreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Return the WebSphere user name.
*/
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpRequest) {
Object principal = WASSecurityHelper.getCurrentUserName();
if (logger.isDebugEnabled()) {
logger.debug("PreAuthenticated WebSphere principal: " + principal);
}
return principal;
}
/**
* For J2EE container-based authentication there is no generic way to
* retrieve the credentials, as such this method returns a fixed dummy
* value.
*/
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpRequest) {
return "N/A";
}
public int getOrder() {
return 0;
}
}
@@ -0,0 +1,24 @@
package org.springframework.security.ui.preauth.websphere;
import org.springframework.security.ui.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
/**
* This AuthenticationDetailsSource implementation, when configured with a MutableGrantedAuthoritiesContainer,
* will set the pre-authenticated granted authorities based on the WebSphere groups for the current WebSphere
* user, mapped using the configured Attributes2GrantedAuthoritiesMapper.
*
* By default, this class is configured to build instances of the
* PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails class.
*
* @author Ruud Senden
*/
public class WebSpherePreAuthenticatedWebAuthenticationDetailsSource extends WebSpherePreAuthenticatedAuthenticationDetailsSource {
/**
* Public constructor which overrides the default AuthenticationDetails
* class to be used.
*/
public WebSpherePreAuthenticatedWebAuthenticationDetailsSource() {
super();
super.setClazz(PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails.class);
}
}
@@ -0,0 +1,84 @@
package org.springframework.security.ui.preauth.x509;
import org.springframework.security.BadCredentialsException;
import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.util.Assert;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.MessageSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.security.cert.X509Certificate;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
/**
* Obtains the principal from a certificate using a regular expression match against the Subject (as returned by a call
* to {@link X509Certificate#getSubjectDN()}).
* <p>
* The regular expression should contain a single group; for example the default expression "CN=(.?)," matches the
* common name field. So "CN=Jimi Hendrix, OU=..." will give a user name of "Jimi Hendrix".
* <p>
* The matches are case insensitive. So "emailAddress=(.?)," will match "EMAILADDRESS=jimi@hendrix.org, CN=..." giving a
* user name "jimi@hendrix.org"
*
* @author Luke Taylor
* @version $Id$
*/
public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor {
//~ Instance fields ================================================================================================
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private Pattern subjectDnPattern;
public SubjectDnX509PrincipalExtractor() {
setSubjectDnRegex("CN=(.*?),");
}
public Object extractPrincipal(X509Certificate clientCert) {
//String subjectDN = clientCert.getSubjectX500Principal().getName();
String subjectDN = clientCert.getSubjectDN().getName();
logger.debug("Subject DN is '" + subjectDN + "'");
Matcher matcher = subjectDnPattern.matcher(subjectDN);
if (!matcher.find()) {
throw new BadCredentialsException(messages.getMessage("DaoX509AuthoritiesPopulator.noMatching",
new Object[] {subjectDN}, "No matching pattern was found in subject DN: {0}"));
}
if (matcher.groupCount() != 1) {
throw new IllegalArgumentException("Regular expression must contain a single group ");
}
String username = matcher.group(1);
logger.debug("Extracted Principal name is '" + username + "'");
return username;
}
/**
* Sets the regular expression which will by used to extract the user name from the certificate's Subject
* DN.
* <p>
* It should contain a single group; for example the default expression "CN=(.?)," matches the common
* name field. So "CN=Jimi Hendrix, OU=..." will give a user name of "Jimi Hendrix".
* <p>
* The matches are case insensitive. So "emailAddress=(.?)," will match "EMAILADDRESS=jimi@hendrix.org,
* CN=..." giving a user name "jimi@hendrix.org"
*
* @param subjectDnRegex the regular expression to find in the subject
*/
public void setSubjectDnRegex(String subjectDnRegex) {
Assert.hasText(subjectDnRegex, "Regular expression may not be null or empty");
subjectDnPattern = Pattern.compile(subjectDnRegex, Pattern.CASE_INSENSITIVE);
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
}
@@ -0,0 +1,55 @@
package org.springframework.security.ui.preauth.x509;
import org.springframework.security.ui.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.ui.FilterChainOrder;
import javax.servlet.http.HttpServletRequest;
import java.security.cert.X509Certificate;
/**
* @author Luke Taylor
* @version $Id$
*/
public class X509PreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
private X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
X509Certificate cert = extractClientCertificate(request);
if (cert == null) {
return null;
}
return principalExtractor.extractPrincipal(cert);
}
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return extractClientCertificate(request);
}
private X509Certificate extractClientCertificate(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
if (logger.isDebugEnabled()) {
logger.debug("X.509 client authentication certificate:" + certs[0]);
}
return certs[0];
}
if (logger.isDebugEnabled()) {
logger.debug("No client certificate found in request.");
}
return null;
}
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
this.principalExtractor = principalExtractor;
}
public int getOrder() {
return FilterChainOrder.X509_FILTER;
}
}
@@ -0,0 +1,17 @@
package org.springframework.security.ui.preauth.x509;
import java.security.cert.X509Certificate;
/**
* Obtains the principal from an X509Certificate for use within the framework.
*
* @author Luke Taylor
* @version $Id$
*/
public interface X509PrincipalExtractor {
/**
* Returns the principal (usually a String) for the given certificate.
*/
Object extractPrincipal(X509Certificate cert);
}
@@ -0,0 +1,384 @@
package org.springframework.security.ui.rememberme;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.Authentication;
import org.springframework.security.SpringSecurityMessageSource;
import org.springframework.security.AccountStatusException;
import org.springframework.security.providers.rememberme.RememberMeAuthenticationToken;
import org.springframework.security.ui.AuthenticationDetailsSource;
import org.springframework.security.ui.WebAuthenticationDetailsSource;
import org.springframework.security.ui.logout.LogoutHandler;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.security.userdetails.UserDetailsChecker;
import org.springframework.security.userdetails.checker.AccountStatusUserDetailsChecker;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Base class for RememberMeServices implementations.
*
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY = "SPRING_SECURITY_REMEMBER_ME_COOKIE";
public static final String DEFAULT_PARAMETER = "_spring_security_remember_me";
public static final int TWO_WEEKS_S = 1209600;
private static final String DELIMITER = ":";
//~ Instance fields ================================================================================================
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String cookieName = SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
private String parameter = DEFAULT_PARAMETER;
private boolean alwaysRemember;
private String key;
private int tokenValiditySeconds = TWO_WEEKS_S;
public void afterPropertiesSet() throws Exception {
Assert.hasLength(key);
Assert.hasLength(parameter);
Assert.hasLength(cookieName);
Assert.notNull(userDetailsService);
}
/**
* Template implementation which locates the Spring Security cookie, decodes it into
* a delimited array of tokens and submits it to subclasses for processing
* via the <tt>processAutoLoginCookie</tt> method.
* <p>
* The returned username is then used to load the UserDetails object for the user, which in turn
* is used to create a valid authentication token.
*/
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
} catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
} catch (UsernameNotFoundException noUser) {
cancelCookie(request, response);
logger.debug("Remember-me login was valid but corresponding user not found.", noUser);
return null;
} catch (InvalidCookieException invalidCookie) {
cancelCookie(request, response);
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
return null;
} catch (AccountStatusException statusInvalid) {
cancelCookie(request, response);
logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
return null;
} catch (RememberMeAuthenticationException e) {
cancelCookie(request, response);
logger.debug(e.getMessage());
return null;
}
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
/**
* Locates the Spring Security remember me cookie in the request and returns its value.
*
* @param request the submitted request which is to be authenticated
* @return the cookie value (if present), null otherwise.
*/
protected String extractRememberMeCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if ((cookies == null) || (cookies.length == 0)) {
return null;
}
for (int i = 0; i < cookies.length; i++) {
if (cookieName.equals(cookies[i].getName())) {
return cookies[i].getValue();
}
}
return null;
}
/**
* Creates the final <tt>Authentication</tt> object returned from the <tt>autoLogin</tt> method.
* <p>
* By default it will create a <tt>RememberMeAuthenticationToken</tt> instance.
*
* @param request the original request. The configured <tt>AuthenticationDetailsSource</tt> will
* use this to build the details property of the returned object.
* @param user the <tt>UserDetails</tt> loaded from the <tt>UserDetailsService</tt>. This will be
* stored as the principal.
*
* @return the <tt>Authentication</tt> for the remember-me authenticated user
*/
protected Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) {
RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user, user.getAuthorities());
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
/**
* Decodes the cookie and splits it into a set of token strings using the ":" delimiter.
*
* @param cookieValue the value obtained from the submitted cookie
* @return the array of tokens.
* @throws InvalidCookieException if the cookie was not base64 encoded.
*/
protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
for (int j = 0; j < cookieValue.length() % 4; j++) {
cookieValue = cookieValue + "=";
}
if (!Base64.isArrayByteBase64(cookieValue.getBytes())) {
throw new InvalidCookieException( "Cookie token was not Base64 encoded; value was '" + cookieValue + "'");
}
String cookieAsPlainText = new String(Base64.decodeBase64(cookieValue.getBytes()));
return StringUtils.delimitedListToStringArray(cookieAsPlainText, DELIMITER);
}
/**
* Inverse operation of decodeCookie.
*
* @param cookieTokens the tokens to be encoded.
* @return base64 encoding of the tokens concatenated with the ":" delimiter.
*/
protected String encodeCookie(String[] cookieTokens) {
StringBuffer sb = new StringBuffer();
for(int i=0; i < cookieTokens.length; i++) {
sb.append(cookieTokens[i]);
if (i < cookieTokens.length - 1) {
sb.append(DELIMITER);
}
}
String value = sb.toString();
sb = new StringBuffer(new String(Base64.encodeBase64(value.getBytes())));
while (sb.charAt(sb.length() - 1) == '=') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
public final void loginFail(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Interactive login attempt was unsuccessful.");
cancelCookie(request, response);
onLoginFail(request, response);
}
protected void onLoginFail(HttpServletRequest request, HttpServletResponse response) {}
/**
* Examines the incoming request and checks for the presence of the configured "remember me" parameter.
* If it's present, or if <tt>alwaysRemember</tt> is set to true, calls <tt>onLoginSucces</tt>.
*/
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
/**
* Called from loginSuccess when a remember-me login has been requested.
* Typically implemented by subclasses to set a remember-me cookie and potentially store a record
* of it if the implementation requires this.
*/
protected abstract void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
/**
* Allows customization of whether a remember-me login has been requested.
* The default is to return true if <tt>alwaysRemember</tt> is set or the configured parameter name has
* been included in the request and is set to the value "true".
*
* @param request the request submitted from an interactive login, which may include additional information
* indicating that a persistent login is desired.
* @param parameter the configured remember-me parameter name.
*
* @return true if the request includes information indicating that a persistent login has been
* requested.
*/
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
String paramValue = request.getParameter(parameter);
if (paramValue != null) {
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") ||
paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
}
return false;
}
/**
* Called from autoLogin to process the submitted persistent login cookie. Subclasses should
* validate the cookie and perform any additional management required.
*
* @param cookieTokens the decoded and tokenized cookie value
* @param request the request
* @param response the response, to allow the cookie to be modified if required.
* @return the UserDetails for the corresponding user account if the cookie was validated successfully.
* @throws RememberMeAuthenticationException if the cookie is invalid or the login is invalid for some
* other reason.
* @throws UsernameNotFoundException if the user account corresponding to the login cookie couldn't be found
* (for example if the user has been removed from the system).
*/
protected abstract UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) throws RememberMeAuthenticationException, UsernameNotFoundException;
/**
* Sets a "cancel cookie" (with maxAge = 0) on the response to disable persistent logins.
*
* @param request
* @param response
*/
protected void cancelCookie(HttpServletRequest request, HttpServletResponse response) {
logger.debug("Cancelling cookie");
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
}
/**
* Sets the cookie on the response
*
* @param tokens the tokens which will be encoded to make the cookie value.
* @param maxAge the value passed to {@link Cookie#setMaxAge(int)}
* @param request the request
* @param response the response to add the cookie to.
*/
protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
String cookieValue = encodeCookie(tokens);
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(maxAge);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
response.addCookie(cookie);
}
/**
* Implementation of <tt>LogoutHandler</tt>. Default behaviour is to call <tt>cancelCookie()</tt>.
*/
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (logger.isDebugEnabled()) {
logger.debug( "Logout of user "
+ (authentication == null ? "Unknown" : authentication.getName()));
}
cancelCookie(request, response);
}
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
protected String getCookieName() {
return cookieName;
}
public void setAlwaysRemember(boolean alwaysRemember) {
this.alwaysRemember = alwaysRemember;
}
/**
* Sets the name of the parameter which should be checked for to see if a remember-me has been requested
* during a login request. This should be the same name you assign to the checkbox in your login form.
*
* @param parameter the HTTP request parameter
*/
public void setParameter(String parameter) {
Assert.hasText(parameter, "Parameter name cannot be null");
this.parameter = parameter;
}
public String getParameter() {
return parameter;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
Assert.notNull(userDetailsService, "UserDetailsService canot be null");
this.userDetailsService = userDetailsService;
}
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public void setTokenValiditySeconds(int tokenValiditySeconds) {
this.tokenValiditySeconds = tokenValiditySeconds;
}
protected int getTokenValiditySeconds() {
return tokenValiditySeconds;
}
protected AuthenticationDetailsSource getAuthenticationDetailsSource() {
return authenticationDetailsSource;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
}
@@ -0,0 +1,11 @@
package org.springframework.security.ui.rememberme;
/**
* @author Luke Taylor
* @version $Id$
*/
public class CookieTheftException extends RememberMeAuthenticationException {
public CookieTheftException(String message) {
super(message);
}
}
@@ -0,0 +1,56 @@
package org.springframework.security.ui.rememberme;
import org.springframework.dao.DataIntegrityViolationException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Simple <tt>PersistentTokenRepository</tt> implementation backed by a Map. Intended for testing only.
*
* @author Luke Taylor
* @version $Id$
*/
public class InMemoryTokenRepositoryImpl implements PersistentTokenRepository {
private Map<String, PersistentRememberMeToken> seriesTokens = new HashMap<String, PersistentRememberMeToken>();
public synchronized void createNewToken(PersistentRememberMeToken token) {
PersistentRememberMeToken current = seriesTokens.get(token.getSeries());
if (current != null) {
throw new DataIntegrityViolationException("Series Id '"+ token.getSeries() +"' already exists!");
}
seriesTokens.put(token.getSeries(), token);
}
public synchronized void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentRememberMeToken token = getTokenForSeries(series);
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), series, tokenValue,
new Date());
// Store it, overwriting the existing one.
seriesTokens.put(series, newToken);
}
public synchronized PersistentRememberMeToken getTokenForSeries(String seriesId) {
return (PersistentRememberMeToken) seriesTokens.get(seriesId);
}
public synchronized void removeUserTokens(String username) {
Iterator<String> series = seriesTokens.keySet().iterator();
while (series.hasNext()) {
Object seriesId = series.next();
PersistentRememberMeToken token = (PersistentRememberMeToken) seriesTokens.get(seriesId);
if (username.equals(token.getUsername())) {
series.remove();
}
}
}
}
@@ -0,0 +1,14 @@
package org.springframework.security.ui.rememberme;
/**
* Exception thrown by a RememberMeServices implementation to indicate
* that a submitted cookie is of an invalid format or has expired.
*
* @author Luke Taylor
* @version $Id$
*/
public class InvalidCookieException extends RememberMeAuthenticationException {
public InvalidCookieException(String message) {
super(message);
}
}
@@ -0,0 +1,160 @@
package org.springframework.security.ui.rememberme;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springframework.jdbc.object.SqlUpdate;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Date;
/**
* JDBC based persistent login token repository implementation.
*
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
//~ Static fields/initializers =====================================================================================
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL =
"create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, " +
"token varchar(64) not null, last_used timestamp not null)";
/** The default SQL used by the <tt>getTokenBySeries</tt> query */
public static final String DEF_TOKEN_BY_SERIES_SQL =
"select username,series,token,last_used from persistent_logins where series = ?";
/** The default SQL used by <tt>createNewToken</tt> */
public static final String DEF_INSERT_TOKEN_SQL =
"insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
/** The default SQL used by <tt>updateToken</tt> */
public static final String DEF_UPDATE_TOKEN_SQL =
"update persistent_logins set token = ?, last_used = ? where series = ?";
/** The default SQL used by <tt>removeUserTokens</tt> */
public static final String DEF_REMOVE_USER_TOKENS_SQL =
"delete from persistent_logins where username = ?";
//~ Instance fields ================================================================================================
private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
private boolean createTableOnStartup;
private MappingSqlQuery<PersistentRememberMeToken> tokensBySeriesMapping;
private SqlUpdate insertToken;
private SqlUpdate updateToken;
private SqlUpdate removeUserTokens;
protected void initDao() {
tokensBySeriesMapping = new TokensBySeriesMapping(getDataSource());
insertToken = new InsertToken(getDataSource());
updateToken = new UpdateToken(getDataSource());
removeUserTokens = new RemoveUserTokens(getDataSource());
if (createTableOnStartup) {
getJdbcTemplate().execute(CREATE_TABLE_SQL);
}
}
public void createNewToken(PersistentRememberMeToken token) {
insertToken.update(
new Object[] {token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate()});
}
public void updateToken(String series, String tokenValue, Date lastUsed) {
updateToken.update(new Object[] {tokenValue, new Date(), series});
}
/**
* Loads the token data for the supplied series identifier.
*
* If an error occurs, it will be reported and null will be returned (since the result should just be a failed
* persistent login).
*
* @param seriesId
* @return the token matching the series, or null if no match found or an exception occurred.
*/
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
try {
return (PersistentRememberMeToken) tokensBySeriesMapping.findObject(seriesId);
} catch(IncorrectResultSizeDataAccessException moreThanOne) {
logger.error("Querying token for series '" + seriesId + "' returned more than one value. Series" +
"should be unique");
} catch(DataAccessException e) {
logger.error("Failed to load token for series " + seriesId, e);
}
return null;
}
public void removeUserTokens(String username) {
removeUserTokens.update(username);
}
/**
* Intended for convenience in debugging. Will create the persistent_tokens database table when the class
* is initialized during the initDao method.
*
* @param createTableOnStartup set to true to execute the
*/
public void setCreateTableOnStartup(boolean createTableOnStartup) {
this.createTableOnStartup = createTableOnStartup;
}
//~ Inner Classes ==================================================================================================
private class TokensBySeriesMapping extends MappingSqlQuery<PersistentRememberMeToken> {
protected TokensBySeriesMapping(DataSource ds) {
super(ds, tokensBySeriesSql);
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}
protected PersistentRememberMeToken mapRow(ResultSet rs, int rowNum) throws SQLException {
PersistentRememberMeToken token =
new PersistentRememberMeToken(rs.getString(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4));
return token;
}
}
private class UpdateToken extends SqlUpdate {
public UpdateToken(DataSource ds) {
super(ds, updateTokenSql);
setMaxRowsAffected(1);
declareParameter(new SqlParameter(Types.VARCHAR));
declareParameter(new SqlParameter(Types.TIMESTAMP));
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}
}
private class InsertToken extends SqlUpdate {
public InsertToken(DataSource ds) {
super(ds, insertTokenSql);
declareParameter(new SqlParameter(Types.VARCHAR));
declareParameter(new SqlParameter(Types.VARCHAR));
declareParameter(new SqlParameter(Types.VARCHAR));
declareParameter(new SqlParameter(Types.TIMESTAMP));
compile();
}
}
private class RemoveUserTokens extends SqlUpdate {
public RemoveUserTokens(DataSource ds) {
super(ds, removeUserTokensSql);
declareParameter(new SqlParameter(Types.VARCHAR));
compile();
}
}
}
@@ -0,0 +1,42 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.rememberme;
import org.springframework.security.Authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Implementation of {@link NullRememberMeServices} that does nothing.<p>Used as a default by several framework
* classes.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class NullRememberMeServices implements RememberMeServices {
//~ Methods ========================================================================================================
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
return null;
}
public void loginFail(HttpServletRequest request, HttpServletResponse response) {}
public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {}
}
@@ -0,0 +1,37 @@
package org.springframework.security.ui.rememberme;
import java.util.Date;
/**
* @author Luke Taylor
* @version $Id$
*/
public class PersistentRememberMeToken {
private String username;
private String series;
private String tokenValue;
private Date date;
public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) {
this.username = username;
this.series = series;
this.tokenValue = tokenValue;
this.date = date;
}
public String getUsername() {
return username;
}
public String getSeries() {
return series;
}
public String getTokenValue() {
return tokenValue;
}
public Date getDate() {
return date;
}
}
@@ -0,0 +1,180 @@
package org.springframework.security.ui.rememberme;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.springframework.dao.DataAccessException;
import org.springframework.security.Authentication;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.util.Assert;
/**
* {@link RememberMeServices} implementation based on Barry Jaspan's
* <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved Persistent Login Cookie
* Best Practice</a>.
*
* There is a slight modification to the described approach, in that the username is not stored as part of the cookie
* but obtained from the persistent store via an implementation of {@link PersistentTokenRepository}. The latter
* should place a unique constraint on the series identifier, so that it is impossible for the same identifier to be
* allocated to two different users.
*
* <p>User management such as changing passwords, removing users and setting user status should be combined
* with maintenance of the user's persistent tokens.
* </p>
*
* <p>Note that while this class will use the date a token was created to check whether a presented cookie
* is older than the configured <tt>tokenValiditySeconds</tt> property and deny authentication in this case,
* it will not delete these tokens from storage. A suitable batch process should be run periodically to
* remove expired tokens from the database.
* </p>
*
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
private SecureRandom random;
public static final int DEFAULT_SERIES_LENGTH = 16;
public static final int DEFAULT_TOKEN_LENGTH = 16;
private int seriesLength = DEFAULT_SERIES_LENGTH;
private int tokenLength = DEFAULT_TOKEN_LENGTH;
public PersistentTokenBasedRememberMeServices() throws Exception {
random = SecureRandom.getInstance("SHA1PRNG");
}
/**
* Locates the presented cookie data in the token repository, using the series id.
* If the data compares successfully with that in the persistent store, a new token is generated and stored with
* the same series. The corresponding cookie value is set on the response.
*
* @param cookieTokens the series and token values
*
* @throws RememberMeAuthenticationException if there is no stored token corresponding to the submitted cookie, or
* if the token in the persistent store has expired.
* @throws InvalidCookieException if the cookie doesn't have two tokens as expected.
* @throws CookieTheftException if a presented series value is found, but the stored token is different from the
* one presented.
*/
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 +
" tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
}
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
if (token.getDate().getTime() + getTokenValiditySeconds()*1000 < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
// Token also matches, so login is valid. Update the token value, keeping the *same* series number.
if (logger.isDebugEnabled()) {
logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" +
token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(),
token.getSeries(), generateTokenData(), new Date());
try {
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
} catch (DataAccessException e) {
logger.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
UserDetails user = getUserDetailsService().loadUserByUsername(token.getUsername());
return user;
}
/**
* Creates a new persistent login token with a new series number, stores the data in the
* persistent token repository and adds the corresponding cookie to the response.
*
*/
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
} catch (DataAccessException e) {
logger.error("Failed to save persistent token ", e);
}
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
super.logout(request, response, authentication);
tokenRepository.removeUserTokens(authentication.getName());
}
protected String generateSeriesData() {
byte[] newSeries = new byte[seriesLength];
random.nextBytes(newSeries);
return new String(Base64.encodeBase64(newSeries));
}
protected String generateTokenData() {
byte[] newToken = new byte[tokenLength];
random.nextBytes(newToken);
return new String(Base64.encodeBase64(newToken));
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
setCookie(new String[] {token.getSeries(), token.getTokenValue()},getTokenValiditySeconds(), request, response);
}
public void setTokenRepository(PersistentTokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
}
public void setSeriesLength(int seriesLength) {
this.seriesLength = seriesLength;
}
public void setTokenLength(int tokenLength) {
this.tokenLength = tokenLength;
}
@Override
public void setTokenValiditySeconds(int tokenValiditySeconds) {
Assert.isTrue(tokenValiditySeconds > 0, "tokenValiditySeconds must be positive for this implementation");
super.setTokenValiditySeconds(tokenValiditySeconds);
}
}
@@ -0,0 +1,26 @@
package org.springframework.security.ui.rememberme;
import java.util.Date;
/**
* The abstraction used by {@link PersistentTokenBasedRememberMeServices} to store the persistent
* login tokens for a user.
*
* @see JdbcTokenRepositoryImpl
* @see InMemoryTokenRepositoryImpl
*
* @author Luke Taylor
* @version $Id$
* @since 2.0
*/
public interface PersistentTokenRepository {
void createNewToken(PersistentRememberMeToken token);
void updateToken(String series, String tokenValue, Date lastUsed);
PersistentRememberMeToken getTokenForSeries(String seriesId);
void removeUserTokens(String username);
}
@@ -0,0 +1,14 @@
package org.springframework.security.ui.rememberme;
import org.springframework.security.AuthenticationException;
/**
* @author Luke Taylor
* @version $Id$
*/
public class RememberMeAuthenticationException extends AuthenticationException {
public RememberMeAuthenticationException(String message) {
super(message);
}
}
@@ -0,0 +1,156 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.rememberme;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;
import org.springframework.security.AuthenticationManager;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.ui.FilterChainOrder;
import org.springframework.security.ui.SpringSecurityFilter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Detects if there is no <code>Authentication</code> object in the <code>SecurityContext</code>, and populates it
* with a remember-me authentication token if a {@link org.springframework.security.ui.rememberme.RememberMeServices}
* implementation so requests.<p>Concrete <code>RememberMeServices</code> implementations will have their {@link
* org.springframework.security.ui.rememberme.RememberMeServices#autoLogin(HttpServletRequest, HttpServletResponse)} method
* called by this filter. The <code>Authentication</code> or <code>null</code> returned by that method will be placed
* into the <code>SecurityContext</code>. The <code>AuthenticationManager</code> will be used, so that any concurrent
* session management or other authentication-specific behaviour can be achieved. This is the same pattern as with
* other authentication mechanisms, which call the <code>AuthenticationManager</code> as part of their contract.</p>
* <p>If authentication is successful, an {@link
* org.springframework.security.event.authentication.InteractiveAuthenticationSuccessEvent} will be published to the application
* context. No events will be published if authentication was unsuccessful, because this would generally be recorded
* via an <code>AuthenticationManager</code>-specific application event.</p>
*
* @author Ben Alex
* @version $Id$
*/
public class RememberMeProcessingFilter extends SpringSecurityFilter implements InitializingBean,
ApplicationEventPublisherAware {
//~ Instance fields ================================================================================================
private ApplicationEventPublisher eventPublisher;
private AuthenticationManager authenticationManager;
private RememberMeServices rememberMeServices;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(authenticationManager, "authenticationManager must be specified");
Assert.notNull(rememberMeServices, "rememberMeServices must be specified");
}
public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
} catch (AuthenticationException authenticationException) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as "
+ "AuthenticationManager rejected Authentication returned by RememberMeServices: '"
+ rememberMeAuth + "'; invalidating remember-me token", authenticationException);
}
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, authenticationException);
}
}
chain.doFilter(request, response);
} else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
/**
* Called if a remember-me token is presented and successfully authenticated by the <tt>RememberMeServices</tt>
* <tt>autoLogin</tt> method and the <tt>AuthenticationManager</tt>.
*/
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) {
}
/**
* Called if the <tt>AuthenticationManager</tt> rejects the authentication object returned from the
* <tt>RememberMeServices</tt> <tt>autoLogin</tt> method. This method will not be called when no remember-me
* token is present in the request and <tt>autoLogin</tt> returns null.
*/
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) {
}
public RememberMeServices getRememberMeServices() {
return rememberMeServices;
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
this.rememberMeServices = rememberMeServices;
}
public int getOrder() {
return FilterChainOrder.REMEMBER_ME_FILTER;
}
}
@@ -0,0 +1,92 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.rememberme;
import org.springframework.security.Authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Implement by a class that is capable of providing a remember-me service.
*
* <p>
* Spring Security filters (namely {@link org.springframework.security.ui.AbstractProcessingFilter} and
* {@link org.springframework.security.ui.rememberme.RememberMeProcessingFilter} will call
* the methods provided by an implementation of this interface.
* <p>
* Implementations may implement any type of remember-me capability they wish.
* Rolling cookies (as per <a href="http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice">
* http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice</a>)
* can be used, as can simple implementations that don't require a persistent store. Implementations also determine
* the validity period of a remember-me cookie. This interface has been designed to accommodate any of these
* remember-me models.
* <p>
* This interface does not define how remember-me services should offer a "cancel all remember-me tokens" type
* capability, as this will be implementation specific and requires no hooks into Spring Security.
*
* @author Ben Alex
* @version $Id$
*/
public interface RememberMeServices {
//~ Methods ========================================================================================================
/**
* This method will be called whenever the <code>SecurityContextHolder</code> does not contain an
* <code>Authentication</code> object and Spring Security wishes to provide an implementation with an
* opportunity to authenticate the request using remember-me capabilities. Spring Security makes no attempt
* whatsoever to determine whether the browser has requested remember-me services or presented a valid cookie.
* Such determinations are left to the implementation. If a browser has presented an unauthorised cookie for
* whatever reason, it should be silently ignored and invalidated using the <code>HttpServletResponse</code>
* object.
* <p>
* The returned <code>Authentication</code> must be acceptable to
* {@link org.springframework.security.AuthenticationManager} or
* {@link org.springframework.security.providers.AuthenticationProvider} defined by the web application.
* It is recommended {@link org.springframework.security.providers.rememberme.RememberMeAuthenticationToken} be
* used in most cases, as it has a corresponding authentication provider.
*
* @param request to look for a remember-me token within
* @param response to change, cancel or modify the remember-me token
*
* @return a valid authentication object, or <code>null</code> if the request should not be authenticated
*/
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
/**
* Called whenever an interactive authentication attempt was made, but the credentials supplied by the user
* were missing or otherwise invalid. Implementations should invalidate any and all remember-me tokens indicated
* in the <code>HttpServletRequest</code>.
*
* @param request that contained an invalid authentication request
* @param response to change, cancel or modify the remember-me token
*/
void loginFail(HttpServletRequest request, HttpServletResponse response);
/**
* Called whenever an interactive authentication attempt is successful. An implementation may automatically
* set a remember-me token in the <code>HttpServletResponse</code>, although this is not recommended. Instead,
* implementations should typically look for a request parameter that indicates the browser has presented an
* explicit request for authentication to be remembered, such as the presence of a HTTP POST parameter.
*
* @param request that contained the valid authentication request
* @param response to change, cancel or modify the remember-me token
* @param successfulAuthentication representing the successfully authenticated principal
*/
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
@@ -0,0 +1,210 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.rememberme;
import org.springframework.security.Authentication;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
/**
* Identifies previously remembered users by a Base-64 encoded cookie.
*
* <p>
* This implementation does not rely on an external database, so is attractive
* for simple applications. The cookie will be valid for a specific period from
* the date of the last {@link #loginSuccess(HttpServletRequest, HttpServletResponse, Authentication)}.
* As per the interface contract, this method will only be called when the
* principal completes a successful interactive authentication. As such the time
* period commences from the last authentication attempt where they furnished
* credentials - not the time period they last logged in via remember-me. The
* implementation will only send a remember-me token if the parameter defined by
* {@link #setParameter(String)} is present.
* <p>
* An {@link org.springframework.security.userdetails.UserDetailsService} is required by
* this implementation, so that it can construct a valid
* <code>Authentication</code> from the returned {@link org.springframework.security.userdetails.UserDetails}.
* This is also necessary so that the user's password is available and can be checked as part of the encoded cookie.
* <p>
* The cookie encoded by this implementation adopts the following form:
*
* <pre>
* username + &quot;:&quot; + expiryTime + &quot;:&quot; + Md5Hex(username + &quot;:&quot; + expiryTime + &quot;:&quot; + password + &quot;:&quot; + key)
* </pre>
*
* <p>
* As such, if the user changes their password, any remember-me token will be
* invalidated. Equally, the system administrator may invalidate every
* remember-me token on issue by changing the key. This provides some reasonable
* approaches to recovering from a remember-me token being left on a public
* machine (e.g. kiosk system, Internet cafe etc). Most importantly, at no time is
* the user's password ever sent to the user agent, providing an important
* security safeguard. Unfortunately the username is necessary in this
* implementation (as we do not want to rely on a database for remember-me
* services). High security applications should be aware of this occasionally undesired
* disclosure of a valid username.
* <p>
* This is a basic remember-me implementation which is suitable for many
* applications. However, we recommend a database-based implementation if you
* require a more secure remember-me approach (see {@link PersistentTokenBasedRememberMeServices}).
* <p>
* By default the tokens will be valid for 14 days from the last successful authentication attempt. This can be changed
* using {@link #setTokenValiditySeconds(int)}. If this value is less than zero, the <tt>expiryTime</tt> will remain at
* 14 days, but the negative value will be used for the <tt>maxAge</tt> property of the cookie, meaning that it will
* not be stored when the browser is closed.
*
*
* @author Ben Alex
* @version $Id$
*/
public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
//~ Methods ========================================================================================================
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
if (cookieTokens.length != 3) {
throw new InvalidCookieException("Cookie token did not contain " + 2 +
" tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
long tokenExpiryTime;
try {
tokenExpiryTime = new Long(cookieTokens[1]).longValue();
}
catch (NumberFormatException nfe) {
throw new InvalidCookieException("Cookie token[1] did not contain a valid number (contained '" +
cookieTokens[1] + "')");
}
if (isTokenExpired(tokenExpiryTime)) {
throw new InvalidCookieException("Cookie token[1] has expired (expired on '"
+ new Date(tokenExpiryTime) + "'; current time is '" + new Date() + "')");
}
// Check the user exists.
// Defer lookup until after expiry time checked, to possibly avoid expensive database call.
UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
// Check signature of token matches remaining details.
// Must do this after user lookup, as we need the DAO-derived password.
// If efficiency was a major issue, just add in a UserCache implementation,
// but recall that this method is usually only called once per HttpSession - if the token is valid,
// it will cause SecurityContextHolder population, whilst if invalid, will cause the cookie to be cancelled.
String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
userDetails.getPassword());
if (!expectedTokenSignature.equals(cookieTokens[2])) {
throw new InvalidCookieException("Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'");
}
return userDetails;
}
/**
* Calculates the digital signature to be put in the cookie. Default value is
* MD5 ("username:tokenExpiryTime:password:key")
*/
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
return DigestUtils.md5Hex(username + ":" + tokenExpiryTime + ":" + password + ":" + getKey());
}
protected boolean isTokenExpired(long tokenExpiryTime) {
return tokenExpiryTime < System.currentTimeMillis();
}
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// If unable to find a username and password, just abort as TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return;
}
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L* (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password);
setCookie(new String[] {username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
if (logger.isDebugEnabled()) {
logger.debug("Added remember-me cookie for user '" + username + "', expiry: '"
+ new Date(expiryTime) + "'");
}
}
/**
* Calculates the validity period in seconds for a newly generated remember-me login.
* After this period (from the current time) the remember-me login will be considered expired.
* This method allows customization based on request parameters supplied with the login or information in
* the <tt>Authentication</tt> object. The default value is just the token validity period property,
* <tt>tokenValiditySeconds</tt>.
* <p>
* The returned value will be used to work out the expiry time of the token and will also be
* used to set the <tt>maxAge</tt> property of the cookie.
*
* See SEC-485.
*
* @param request the request passed to onLoginSuccess
* @param authentication the successful authentication object.
* @return the lifetime in seconds.
*/
protected int calculateLoginLifetime(HttpServletRequest request, Authentication authentication) {
return getTokenValiditySeconds();
}
protected String retrieveUserName(Authentication authentication) {
if (isInstanceOfUserDetails(authentication)) {
return ((UserDetails) authentication.getPrincipal()).getUsername();
}
else {
return authentication.getPrincipal().toString();
}
}
protected String retrievePassword(Authentication authentication) {
if (isInstanceOfUserDetails(authentication)) {
return ((UserDetails) authentication.getPrincipal()).getPassword();
}
else {
if (authentication.getCredentials() == null) {
return null;
}
return authentication.getCredentials().toString();
}
}
private boolean isInstanceOfUserDetails(Authentication authentication) {
return authentication.getPrincipal() instanceof UserDetails;
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Support for remembering a user between different web sessions.
</body>
</html>
@@ -0,0 +1,139 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.savedrequest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* <p>Adapter that wraps an <code>Enumeration</code> around a Java 2 collection <code>Iterator</code>.</p>
* <p>Constructors are provided to easily create such wrappers.</p>
* <p>This class is based on code in Apache Tomcat.</p>
*
* @author Craig McClanahan
* @author Andrey Grebnev
* @version $Id$
*/
public class Enumerator<T> implements Enumeration<T> {
//~ Instance fields ================================================================================================
/**
* The <code>Iterator</code> over which the <code>Enumeration</code> represented by this class actually operates.
*/
private Iterator<T> iterator = null;
//~ Constructors ===================================================================================================
/**
* Return an Enumeration over the values of the specified Collection.
*
* @param collection Collection whose values should be enumerated
*/
public Enumerator(Collection<T> collection) {
this(collection.iterator());
}
/**
* Return an Enumeration over the values of the specified Collection.
*
* @param collection Collection whose values should be enumerated
* @param clone true to clone iterator
*/
public Enumerator(Collection<T> collection, boolean clone) {
this(collection.iterator(), clone);
}
/**
* Return an Enumeration over the values returned by the specified
* Iterator.
*
* @param iterator Iterator to be wrapped
*/
public Enumerator(Iterator<T> iterator) {
this.iterator = iterator;
}
/**
* Return an Enumeration over the values returned by the specified
* Iterator.
*
* @param iterator Iterator to be wrapped
* @param clone true to clone iterator
*/
public Enumerator(Iterator<T> iterator, boolean clone) {
if (!clone) {
this.iterator = iterator;
} else {
List<T> list = new ArrayList<T>();
while (iterator.hasNext()) {
list.add(iterator.next());
}
this.iterator = list.iterator();
}
}
/**
* Return an Enumeration over the values of the specified Map.
*
* @param map Map whose values should be enumerated
*/
public Enumerator(Map<?, T> map) {
this(map.values().iterator());
}
/**
* Return an Enumeration over the values of the specified Map.
*
* @param map Map whose values should be enumerated
* @param clone true to clone iterator
*/
public Enumerator(Map<?, T> map, boolean clone) {
this(map.values().iterator(), clone);
}
//~ Methods ========================================================================================================
/**
* Tests if this enumeration contains more elements.
*
* @return <code>true</code> if and only if this enumeration object contains at least one more element to provide,
* <code>false</code> otherwise
*/
public boolean hasMoreElements() {
return (iterator.hasNext());
}
/**
* Returns the next element of this enumeration if this enumeration has at least one more element to
* provide.
*
* @return the next element of this enumeration
*
* @exception NoSuchElementException if no more elements exist
*/
public T nextElement() throws NoSuchElementException {
return (iterator.next());
}
}
@@ -0,0 +1,221 @@
/* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.ui.savedrequest;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;
/**
* Utility class to generate HTTP dates.
* <p>
* This class is based on code in Apache Tomcat.
*
* @author Remy Maucherat
* @author Andrey Grebnev
* @version $Id$
*/
public class FastHttpDateFormat {
//~ Static fields/initializers =====================================================================================
/** HTTP date format. */
protected static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
/** The set of SimpleDateFormat formats to use in <code>getDateHeader()</code>. */
protected static final SimpleDateFormat[] formats = {
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
};
/** GMT time zone - all HTTP dates are on GMT */
protected static final TimeZone gmtZone = TimeZone.getTimeZone("GMT");
static {
format.setTimeZone(gmtZone);
formats[0].setTimeZone(gmtZone);
formats[1].setTimeZone(gmtZone);
formats[2].setTimeZone(gmtZone);
}
/** Instant on which the currentDate object was generated. */
protected static long currentDateGenerated = 0L;
/** Current formatted date. */
protected static String currentDate = null;
/** Formatter cache. */
protected static final HashMap<Long,String> formatCache = new HashMap<Long,String>();
/** Parser cache. */
protected static final HashMap<String,Long> parseCache = new HashMap<String,Long>();
//~ Methods ========================================================================================================
/**
* Formats a specified date to HTTP format. If local format is not <code>null</code>, it's used instead.
*
* @param value Date value to format
* @param threadLocalformat The format to use (or <code>null</code> -- then HTTP format will be used)
*
* @return Formatted date
*/
public static final String formatDate(long value, DateFormat threadLocalformat) {
String cachedDate = null;
Long longValue = new Long(value);
try {
cachedDate = formatCache.get(longValue);
} catch (Exception e) {}
if (cachedDate != null) {
return cachedDate;
}
String newDate = null;
Date dateValue = new Date(value);
if (threadLocalformat != null) {
newDate = threadLocalformat.format(dateValue);
synchronized (formatCache) {
updateCache(formatCache, longValue, newDate);
}
} else {
synchronized (formatCache) {
newDate = format.format(dateValue);
updateCache(formatCache, longValue, newDate);
}
}
return newDate;
}
/**
* Gets the current date in HTTP format.
*
* @return Current date in HTTP format
*/
public static final String getCurrentDate() {
long now = System.currentTimeMillis();
if ((now - currentDateGenerated) > 1000) {
synchronized (format) {
if ((now - currentDateGenerated) > 1000) {
currentDateGenerated = now;
currentDate = format.format(new Date(now));
}
}
}
return currentDate;
}
/**
* Parses date with given formatters.
*
* @param value The string to parse
* @param formats Array of formats to use
*
* @return Parsed date (or <code>null</code> if no formatter mached)
*/
private static Long internalParseDate(String value, DateFormat[] formats) {
Date date = null;
for (int i = 0; (date == null) && (i < formats.length); i++) {
try {
date = formats[i].parse(value);
} catch (ParseException e) {
;
}
}
if (date == null) {
return null;
}
return new Long(date.getTime());
}
/**
* Tries to parse the given date as an HTTP date. If local format list is not <code>null</code>, it's used
* instead.
*
* @param value The string to parse
* @param threadLocalformats Array of formats to use for parsing. If <code>null</code>, HTTP formats are used.
*
* @return Parsed date (or -1 if error occurred)
*/
public static final long parseDate(String value, DateFormat[] threadLocalformats) {
Long cachedDate = null;
try {
cachedDate = (Long) parseCache.get(value);
} catch (Exception e) {}
if (cachedDate != null) {
return cachedDate.longValue();
}
Long date = null;
if (threadLocalformats != null) {
date = internalParseDate(value, threadLocalformats);
synchronized (parseCache) {
updateCache(parseCache, value, date);
}
} else {
synchronized (parseCache) {
date = internalParseDate(value, formats);
updateCache(parseCache, value, date);
}
}
if (date == null) {
return (-1L);
} else {
return date.longValue();
}
}
/**
* Updates cache.
*
* @param cache Cache to be updated
* @param key Key to be updated
* @param value New value
*/
@SuppressWarnings("unchecked")
private static void updateCache(HashMap cache, Object key, Object value) {
if (value == null) {
return;
}
if (cache.size() > 1000) {
cache.clear();
}
cache.put(key, value);
}
}
@@ -0,0 +1,86 @@
package org.springframework.security.ui.savedrequest;
import javax.servlet.http.Cookie;
import java.io.Serializable;
/**
* Stores off the values of a cookie in a serializable holder
*
* @author Ray Krueger
*/
public class SavedCookie implements Serializable {
private java.lang.String name;
private java.lang.String value;
private java.lang.String comment;
private java.lang.String domain;
private int maxAge;
private java.lang.String path;
private boolean secure;
private int version;
public SavedCookie(String name, String value, String comment, String domain, int maxAge, String path, boolean secure, int version) {
this.name = name;
this.value = value;
this.comment = comment;
this.domain = domain;
this.maxAge = maxAge;
this.path = path;
this.secure = secure;
this.version = version;
}
public SavedCookie(Cookie cookie) {
this(cookie.getName(), cookie.getValue(), cookie.getComment(),
cookie.getDomain(), cookie.getMaxAge(), cookie.getPath(), cookie.getSecure(), cookie.getVersion());
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public String getComment() {
return comment;
}
public String getDomain() {
return domain;
}
public int getMaxAge() {
return maxAge;
}
public String getPath() {
return path;
}
public boolean isSecure() {
return secure;
}
public int getVersion() {
return version;
}
public Cookie getCookie() {
Cookie c = new Cookie(getName(), getValue());
if (getComment() != null)
c.setComment(getComment());
if (getDomain() != null)
c.setDomain(getDomain());
if (getPath() != null)
c.setPath(getPath());
c.setVersion(getVersion());
c.setMaxAge(getMaxAge());
c.setSecure(isSecure());
return c;
}
}

Some files were not shown because too many files have changed in this diff Show More