|
@@ -0,0 +1,287 @@
|
|
|
+/*
|
|
|
+ * LICENSE IS UNKNOWN (SEE TODO COMMENT LATER IN SOURCE CODE)
|
|
|
+ */
|
|
|
+package net.sf.acegisecurity.ui.ntlm;
|
|
|
+
|
|
|
+import jcifs.Config;
|
|
|
+import jcifs.UniAddress;
|
|
|
+
|
|
|
+import jcifs.http.NtlmSsp;
|
|
|
+
|
|
|
+import jcifs.smb.NtlmChallenge;
|
|
|
+import jcifs.smb.NtlmPasswordAuthentication;
|
|
|
+import jcifs.smb.SmbSession;
|
|
|
+
|
|
|
+import net.sf.acegisecurity.Authentication;
|
|
|
+import net.sf.acegisecurity.AuthenticationException;
|
|
|
+import net.sf.acegisecurity.AuthenticationManager;
|
|
|
+import net.sf.acegisecurity.BadCredentialsException;
|
|
|
+import net.sf.acegisecurity.context.SecurityContextHolder;
|
|
|
+import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
|
|
|
+import net.sf.acegisecurity.providers.smb.NtlmAuthenticationToken;
|
|
|
+
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+
|
|
|
+import org.springframework.beans.factory.InitializingBean;
|
|
|
+
|
|
|
+import org.springframework.util.Assert;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+
|
|
|
+import java.util.Iterator;
|
|
|
+import java.util.Properties;
|
|
|
+
|
|
|
+import javax.servlet.Filter;
|
|
|
+import javax.servlet.FilterChain;
|
|
|
+import javax.servlet.FilterConfig;
|
|
|
+import javax.servlet.ServletException;
|
|
|
+import javax.servlet.ServletRequest;
|
|
|
+import javax.servlet.ServletResponse;
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import javax.servlet.http.HttpSession;
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * A reimplementation of the jcifs NtlmHttpFilter suitable for use with the
|
|
|
+ * Acegi Security System.
|
|
|
+ *
|
|
|
+ * <p>
|
|
|
+ * This servlet Filter can be used to negotiate password hashes with MSIE
|
|
|
+ * clients using NTLM SSP. This is similar to <code>Authentication:
|
|
|
+ * BASIC</code> but weakly encrypted and without requiring the user to
|
|
|
+ * re-supply authentication credentials.
|
|
|
+ * </p>
|
|
|
+ *
|
|
|
+ * @author Davide Baroncelli
|
|
|
+ * @version $Id$
|
|
|
+ */
|
|
|
+public class NtlmProcessingFilter implements Filter, InitializingBean {
|
|
|
+ //~ Static fields/initializers =============================================
|
|
|
+
|
|
|
+ private static final String CHALLENGE_ATTR_NAME = "NtlmHttpChal";
|
|
|
+
|
|
|
+ //~ Instance fields ========================================================
|
|
|
+
|
|
|
+ private AuthenticationEntryPoint authenticationEntryPoint;
|
|
|
+ private AuthenticationManager authenticationManager;
|
|
|
+
|
|
|
+ // TODO: Verify licensing, as original contributor reported that large parts of this code where taken from jCifs NtmlHttpFilter: can this be re-licensed to APL (?).
|
|
|
+ private Log log = LogFactory.getLog(this.getClass());
|
|
|
+ private String defaultDomain;
|
|
|
+ private String domainController;
|
|
|
+ private boolean loadBalance;
|
|
|
+
|
|
|
+ //~ Methods ================================================================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DOCUMENT ME!
|
|
|
+ *
|
|
|
+ * @param authenticationEntryPoint The entry point that will be called if
|
|
|
+ * the "transparent" authentication fails for some reason: don't
|
|
|
+ * use the same {@link NtlmProcessingFilterEntryPoint} that is used
|
|
|
+ * in order to commence the NTLM authentication or the user's
|
|
|
+ * browser would probably loop
|
|
|
+ */
|
|
|
+ public void setAuthenticationEntryPoint(
|
|
|
+ AuthenticationEntryPoint authenticationEntryPoint) {
|
|
|
+ this.authenticationEntryPoint = authenticationEntryPoint;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setAuthenticationManager(
|
|
|
+ AuthenticationManager authenticationManager) {
|
|
|
+ this.authenticationManager = authenticationManager;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DOCUMENT ME!
|
|
|
+ *
|
|
|
+ * @param defaultDomain The domain that will be specified as part of the
|
|
|
+ * authentication credentials if not specified by the username.
|
|
|
+ */
|
|
|
+ public void setDefaultDomain(String defaultDomain) {
|
|
|
+ this.defaultDomain = defaultDomain;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DOCUMENT ME!
|
|
|
+ *
|
|
|
+ * @param domainController The domain controller address: if not set the
|
|
|
+ * default domain is used.
|
|
|
+ */
|
|
|
+ public void setDomainController(String domainController) {
|
|
|
+ this.domainController = domainController;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DOCUMENT ME!
|
|
|
+ *
|
|
|
+ * @param properties A {@link Properties} object whose properties with
|
|
|
+ * names starting with "jcifs." will be set into the jCifs {@link
|
|
|
+ * Config}.
|
|
|
+ */
|
|
|
+ public void setJCifsProperties(Properties properties) {
|
|
|
+ for (Iterator iterator = properties.keySet().iterator();
|
|
|
+ iterator.hasNext();) {
|
|
|
+ String propertyName = (String) iterator.next();
|
|
|
+ String propertyValue = properties.getProperty(propertyName);
|
|
|
+
|
|
|
+ if (propertyName.startsWith("jcifs.")) {
|
|
|
+ if (log.isInfoEnabled()) {
|
|
|
+ log.info("setting jcifs property " + propertyName + ":"
|
|
|
+ + propertyValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ Config.setProperty(propertyName, propertyValue);
|
|
|
+ } else {
|
|
|
+ if (log.isInfoEnabled()) {
|
|
|
+ log.info("ignoring non-jcifs property " + propertyName
|
|
|
+ + ":" + propertyValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setLoadBalance(boolean loadBalance) {
|
|
|
+ this.loadBalance = loadBalance;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void afterPropertiesSet() throws Exception {
|
|
|
+ // Set jcifs properties we know we want; soTimeout and cachePolicy to 10min
|
|
|
+ Config.setProperty("jcifs.smb.client.soTimeout", "300000");
|
|
|
+ Config.setProperty("jcifs.netbios.cachePolicy", "1200");
|
|
|
+
|
|
|
+ if (domainController == null) {
|
|
|
+ domainController = defaultDomain;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (defaultDomain != null) {
|
|
|
+ Config.setProperty("jcifs.smb.client.domain", defaultDomain);
|
|
|
+ }
|
|
|
+
|
|
|
+ Assert.notNull(authenticationEntryPoint,
|
|
|
+ "The authenticationEntryPoint property must be set before "
|
|
|
+ + "NtlmProcessingFilter bean initialization");
|
|
|
+ Assert.notNull(authenticationManager,
|
|
|
+ "The authenticationManager property must be set before "
|
|
|
+ + "NtlmProcessingFilter bean initialization");
|
|
|
+ }
|
|
|
+
|
|
|
+ public void destroy() {}
|
|
|
+
|
|
|
+ public void doFilter(ServletRequest request, ServletResponse response,
|
|
|
+ FilterChain chain) throws IOException, ServletException {
|
|
|
+ NtlmPasswordAuthentication ntlm = null;
|
|
|
+ HttpServletRequest req = (HttpServletRequest) request;
|
|
|
+ HttpServletResponse resp = (HttpServletResponse) response;
|
|
|
+
|
|
|
+ try {
|
|
|
+ String msg = req.getHeader("Authorization");
|
|
|
+
|
|
|
+ UniAddress dc = null;
|
|
|
+
|
|
|
+ // the "basic" authentication case (both secure + insecure) originally in jcifs NtlmFilter has been
|
|
|
+ // refactored out, in order for it to be supported by the acegi BasicProcessingFilter +
|
|
|
+ // SmbNtlmAuthenticationProvider ( + SecureChannelProcessor) combination */
|
|
|
+ if ((msg != null) && (msg.startsWith("NTLM "))) {
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("NTLM Authorization header received");
|
|
|
+ }
|
|
|
+
|
|
|
+ HttpSession ssn = req.getSession();
|
|
|
+ byte[] challenge;
|
|
|
+
|
|
|
+ if (loadBalance) {
|
|
|
+ NtlmChallenge chal = (NtlmChallenge) ssn.getAttribute(CHALLENGE_ATTR_NAME);
|
|
|
+
|
|
|
+ if (chal == null) {
|
|
|
+ chal = SmbSession.getChallengeForDomain();
|
|
|
+
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug(
|
|
|
+ "got load balanced challenge for domain: "
|
|
|
+ + chal);
|
|
|
+ }
|
|
|
+
|
|
|
+ ssn.setAttribute(CHALLENGE_ATTR_NAME, chal);
|
|
|
+ }
|
|
|
+
|
|
|
+ dc = chal.dc;
|
|
|
+ challenge = chal.challenge;
|
|
|
+ } else {
|
|
|
+ // no challenge in session, here: the server itself keeps the challenge alive for a certain time
|
|
|
+ dc = UniAddress.getByName(domainController, true);
|
|
|
+ challenge = SmbSession.getChallenge(dc);
|
|
|
+
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("domain controller is " + dc
|
|
|
+ + ", challenge is " + challenge);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ntlm = NtlmSsp.authenticate(req, resp, challenge);
|
|
|
+
|
|
|
+ if (ntlm == null) {
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug(
|
|
|
+ "null ntlm authentication results: sending challenge to browser");
|
|
|
+ }
|
|
|
+
|
|
|
+ return; // this means we must send the challenge to the browser
|
|
|
+ } else {
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("ntlm negotiation complete");
|
|
|
+ }
|
|
|
+
|
|
|
+ ssn.removeAttribute(CHALLENGE_ATTR_NAME); /* negotiation complete, remove the challenge object */
|
|
|
+ }
|
|
|
+
|
|
|
+ NtlmAuthenticationToken ntlmToken = newNtlmAuthenticationToken(ntlm,
|
|
|
+ dc);
|
|
|
+ Authentication authResult = authenticationManager.authenticate(ntlmToken);
|
|
|
+
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("ntlm token authenticated ");
|
|
|
+ }
|
|
|
+
|
|
|
+ successfulAuthentication(req, resp, authResult);
|
|
|
+ }
|
|
|
+ } catch (AuthenticationException ae) {
|
|
|
+ unsuccessfulAuthentication(req, resp, ntlm, ae);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ chain.doFilter(request, response);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void init(FilterConfig filterConfig) throws ServletException {}
|
|
|
+
|
|
|
+ protected NtlmAuthenticationToken newNtlmAuthenticationToken(
|
|
|
+ NtlmPasswordAuthentication ntlm, UniAddress dc) {
|
|
|
+ return new NtlmAuthenticationToken(ntlm, dc);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected void successfulAuthentication(HttpServletRequest request,
|
|
|
+ HttpServletResponse response, Authentication authResult) {
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("Authentication success: " + authResult.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(authResult);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected void unsuccessfulAuthentication(HttpServletRequest req,
|
|
|
+ HttpServletResponse resp, NtlmPasswordAuthentication ntlm,
|
|
|
+ AuthenticationException ae) throws IOException, ServletException {
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("Authentication request for user: " + ntlm.getUsername()
|
|
|
+ + " failed: " + ae.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(null);
|
|
|
+ authenticationEntryPoint.commence(req, resp,
|
|
|
+ new BadCredentialsException(ae.getMessage(), ae));
|
|
|
+ }
|
|
|
+}
|