Although NTLM pass-through method is not supported for ChainedAuthentication in 2.0, following was achievable:
One of authentication methods, used in chaining can be made available for CIFS 'alfresco' mode by naming this bean as 'authenticationComponent' in configuration file. CIFS ignores override of authenticationService, using authenticationComponent directly.
This setup is useful when only few users are external to the domain that you are using for NTLM pass-through, and only require web access.
It is also possible to use NTLMAuthenticationFilter for with chained authentication, but code has to be slightly changed, recompiled and made available for web application.
Here is a code of updated NTLMAuthenticationFilter.java (version 2):
package org.alfresco.web.app.servlet;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.transaction.UserTransaction;
import net.sf.acegisecurity.BadCredentialsException;
import org.alfresco.config.ConfigService;
import org.alfresco.filesys.server.auth.PasswordEncryptor;
import org.alfresco.filesys.server.auth.ntlm.NTLM;
import org.alfresco.filesys.server.auth.ntlm.NTLMLogonDetails;
import org.alfresco.filesys.server.auth.ntlm.NTLMMessage;
import org.alfresco.filesys.server.auth.ntlm.TargetInfo;
import org.alfresco.filesys.server.auth.ntlm.Type1NTLMMessage;
import org.alfresco.filesys.server.auth.ntlm.Type2NTLMMessage;
import org.alfresco.filesys.server.auth.ntlm.Type3NTLMMessage;
import org.alfresco.filesys.server.config.ServerConfiguration;
import org.alfresco.filesys.util.DataPacker;
import org.alfresco.i18n.I18NUtil;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationComponent;
import org.alfresco.repo.security.authentication.AuthenticationException;
import org.alfresco.repo.security.authentication.MD4PasswordEncoder;
import org.alfresco.repo.security.authentication.MD4PasswordEncoderImpl;
import org.alfresco.repo.security.authentication.NTLMMode;
import org.alfresco.repo.security.authentication.ntlm.NTLMPassthruToken;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.web.app.Application;
import org.alfresco.web.bean.LoginBean;
import org.alfresco.web.bean.repository.User;
import org.alfresco.web.config.LanguagesConfigElement;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* NTLM Authentication Filter Class
*
* @author GKSpencer
*/
public class NTLMAuthenticationFilter extends AbstractAuthenticationFilter implements Filter
{
// NTLM authentication session object names
public static final String NTLM_AUTH_SESSION = '_alfNTLMAuthSess';
public static final String NTLM_AUTH_DETAILS = '_alfNTLMDetails';
// Locale object stored in the session
private static final String LOCALE = 'locale';
public static final String MESSAGE_BUNDLE = 'alfresco.messages.webclient';
// NTLM flags mask, used to mask out features that are not supported
private static final int NTLM_FLAGS = NTLM.Flag56Bit + NTLM.FlagLanManKey + NTLM.FlagNegotiateNTLM +
NTLM.FlagNegotiateOEM + NTLM.FlagNegotiateUnicode;
// Debug logging
private static Log logger = LogFactory.getLog(NTLMAuthenticationFilter.class);
// Servlet context, required to get authentication service
private ServletContext m_context;
// File server configuration
private ServerConfiguration m_srvConfig;
// Various services required by NTLM authenticator
private AuthenticationService m_authService;
private AuthenticationComponent m_authComponent;
private PersonService m_personService;
private NodeService m_nodeService;
private TransactionService m_transactionService;
private ConfigService m_configService;
// Password encryptor
private PasswordEncryptor m_encryptor = new PasswordEncryptor();
// Allow guest access
private boolean m_allowGuest;
// Login page address
private String m_loginPage;
// Random number generator used to generate challenge keys
private Random m_random = new Random(System.currentTimeMillis());
// MD4 hash decoder
private MD4PasswordEncoder m_md4Encoder = new MD4PasswordEncoderImpl();
// Local server name, from either the file servers config or DNS host name
private String m_srvName;
// List of available locales (from the web-client configuration)
private List<String> m_languages;
/**
* Initialize the filter
*
* @param args FilterConfig
* @exception ServletException
*/
public void init(FilterConfig args) throws ServletException
{
// Save the servlet context, needed to get hold of the authentication service
m_context = args.getServletContext();
// Setup the authentication context
WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(m_context);
ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
m_nodeService = serviceRegistry.getNodeService();
m_transactionService = serviceRegistry.getTransactionService();
m_authService = (AuthenticationService) ctx.getBean('AuthenticationService');
m_authComponent = (AuthenticationComponent) ctx.getBean('AuthenticationComponent');
m_personService = (PersonService) ctx.getBean('personService');
m_configService = (ConfigService) ctx.getBean('webClientConfigService');
m_srvConfig = (ServerConfiguration) ctx.getBean(ServerConfiguration.SERVER_CONFIGURATION);
// Check that the authentication component supports the required mode
if ( m_authComponent.getNTLMMode() != NTLMMode.MD4_PROVIDER &&
m_authComponent.getNTLMMode() != NTLMMode.PASS_THROUGH)
{
throw new ServletException('Required authentication mode not available');
}
// Get the local server name, try the file server config first
if ( m_srvConfig != null)
{
m_srvName = m_srvConfig.getServerName();
if ( m_srvName == null)
{
// CIFS server may not be running so the local server name has not been set, generate
// a server name
m_srvName = m_srvConfig.getLocalServerName(true) + '_A';
}
}
else
{
// Get the host name
try
{
// Get the local host name
m_srvName = InetAddress.getLocalHost().getHostName();
// Strip any domain name
int pos = m_srvName.indexOf('.');
if ( pos != -1)
m_srvName = m_srvName.substring(0, pos - 1);
}
catch (UnknownHostException ex)
{
// Log the error
if ( logger.isErrorEnabled())
logger.error('NTLM filter, error getting local host name', ex);
}
}
// Check if the server name is valid
if ( m_srvName == null || m_srvName.length() == 0)
throw new ServletException('Failed to get local server name');
// Check if guest access is to be allowed
String guestAccess = args.getInitParameter('AllowGuest');
if ( guestAccess != null)
{
m_allowGuest = Boolean.parseBoolean(guestAccess);
// Debug
if ( logger.isDebugEnabled() && m_allowGuest)
logger.debug('NTLM filter guest access allowed');
}
// Get a list of the available locales
LanguagesConfigElement config = (LanguagesConfigElement) m_configService.
getConfig('Languages').getConfigElement(LanguagesConfigElement.CONFIG_ELEMENT_ID);
m_languages = config.getLanguages();
}
/**
* Run the filter
*
* @param sreq ServletRequest
* @param sresp ServletResponse
* @param chain FilterChain
* @exception IOException
* @exception ServletException
*/
public void doFilter(ServletRequest sreq, ServletResponse sresp, FilterChain chain) throws IOException,
ServletException
{
// Get the HTTP request/response/session
HttpServletRequest req = (HttpServletRequest) sreq;
HttpServletResponse resp = (HttpServletResponse) sresp;
HttpSession httpSess = req.getSession(true);
// Check if there is an authorization header with an NTLM security blob
String authHdr = req.getHeader('Authorization');
boolean reqAuth = false;
if ( authHdr != null && authHdr.startsWith('NTLM'))
reqAuth = true;
// Check if the user is already authenticated
User user = (User) httpSess.getAttribute(AuthenticationHelper.AUTHENTICATION_USER);
if ( user != null && !reqAuth)
{
try
{
// Debug
authUser(user);
reqAuth = false;
// Set the current locale
I18NUtil.setLocale(Application.getLanguage(httpSess));
}
catch (AuthenticationException ex)
{
if ( logger.isErrorEnabled())
logger.error('Failed to validate user ' + user.getUserName(), ex);
reqAuth = true;
}
}
// If the user has been validated and we do not require re-authentication then continue to
// the next filter
if ( !reqAuth && user != null)
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('Authentication not required, chaining ...');
// Chain to the next filter
chain.doFilter(sreq, sresp);
return;
}
// Check if the login page is being accessed, do not intercept the login page
if ( req.getRequestURI().endsWith(getLoginPage()))
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('Login page requested, chaining ...');
// Chain to the next filter
chain.doFilter( sreq, sresp);
return;
}
// Check if the browser is Opera, if so then display the login page as Opera does not
// support NTLM and displays an error page if a request to use NTLM is sent to it
String userAgent = req.getHeader('user-agent');
if ( userAgent != null && userAgent.indexOf('Opera ') != -1)
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('Opera detected, redirecting to login page');
// Redirect to the login page
resp.sendRedirect(req.getContextPath() + '/faces' + getLoginPage());
return;
}
// Check the authorization header
if ( authHdr == null) {
// Debug
if ( logger.isDebugEnabled())
logger.debug('New NTLM auth request from ' + req.getRemoteHost() + ' (' +
req.getRemoteAddr() + ':' + req.getRemotePort() + ')');
// Send back a request for NTLM authentication
resp.setHeader('WWW-Authenticate', 'NTLM');
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.flushBuffer();
}
else
{
// Decode the received NTLM blob and validate
final byte[] ntlmByts = Base64.decodeBase64( authHdr.substring(5).getBytes());
int ntlmTyp = NTLMMessage.isNTLMType(ntlmByts);
if ( ntlmTyp == NTLM.Type1)
{
// Process the type 1 NTLM message
Type1NTLMMessage type1Msg = new Type1NTLMMessage(ntlmByts);
processType1(type1Msg, req, resp, httpSess);
}
else if ( ntlmTyp == NTLM.Type3)
{
// Process the type 3 NTLM message
Type3NTLMMessage type3Msg = new Type3NTLMMessage(ntlmByts);
processType3(type3Msg, req, resp, httpSess, chain);
}
else
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('NTLM not handled, redirecting to login page');
// Redirect to the login page
resp.sendRedirect(req.getContextPath() + '/faces' + getLoginPage());
}
}
}
/**
* Introduced for Refactoring (Sasha)
*
* @param user
* @throws IOException
* @throws ServletException
*/
private void authUser(User user) throws IOException, ServletException {
if ( logger.isDebugEnabled())
logger.debug('User ' + user.getUserName() + ' validate ticket');
// Validate the user ticket
UserTransaction tx = m_transactionService.getUserTransaction();
try
{
tx.begin();
m_authService.validate( user.getTicket());
// commit
tx.commit();
}
catch (Throwable ex)
{
try
{
tx.rollback();
}
catch (Exception ex2)
{
logger.error('Failed to rollback transaction', ex2);
}
if(ex instanceof RuntimeException)
{
throw (RuntimeException)ex;
}
else if(ex instanceof IOException)
{
throw (IOException)ex;
}
else if(ex instanceof ServletException)
{
throw (ServletException)ex;
}
else
{
throw new RuntimeException('Authentication setup failed', ex);
}
}
}
/**
* Return the login page address
*
* @return String
*/
private String getLoginPage()
{
if (m_loginPage == null)
{
m_loginPage = Application.getLoginPage(m_context);
}
return m_loginPage;
}
/**
* Delete the servlet filter
*/
public void destroy()
{
}
/**
* Process a type 1 NTLM message
*
* @param type1Msg Type1NTLMMessage
* @param req HttpServletRequest
* @param resp HttpServletResponse
* @param httpSess HttpSession
* @exception IOException
*/
private void processType1(Type1NTLMMessage type1Msg, HttpServletRequest req, HttpServletResponse resp,
HttpSession httpSess) throws IOException
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('Received type1 ' + type1Msg);
// Get the existing NTLM details
NTLMLogonDetails ntlmDetails = null;
if ( httpSess != null)
{
ntlmDetails = (NTLMLogonDetails) httpSess.getAttribute(NTLM_AUTH_DETAILS);
}
// Check if cached logon details are available
if ( ntlmDetails != null && ntlmDetails.hasType2Message() && ntlmDetails.hasNTLMHashedPassword() &&
ntlmDetails.hasAuthenticationToken())
{
// Get the authentication server type2 response
Type2NTLMMessage cachedType2 = ntlmDetails.getType2Message();
byte[] type2Bytes = cachedType2.getBytes();
String ntlmBlob = 'NTLM ' + new String(Base64.encodeBase64(type2Bytes));
// Debug
if ( logger.isDebugEnabled())
logger.debug('Sending cached NTLM type2 to client - ' + cachedType2);
// Send back a request for NTLM authentication
resp.setHeader('WWW-Authenticate', ntlmBlob);
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.flushBuffer();
return;
}
else
{
// Clear any cached logon details
httpSess.removeAttribute(NTLM_AUTH_DETAILS);
// Set the 8 byte challenge for the new logon request
byte[] challenge = null;
NTLMPassthruToken authToken = null;
if ( m_authComponent.getNTLMMode() == NTLMMode.MD4_PROVIDER)
{
// Generate a random 8 byte challenge
challenge = new byte[8];
DataPacker.putIntelLong(m_random.nextLong(), challenge, 0);
}
else
{
// Create an authentication token for the new logon
authToken = new NTLMPassthruToken();
// Run the first stage of the passthru authentication to get the challenge
m_authComponent.authenticate( authToken);
// Get the challenge from the token
if ( authToken.getChallenge() != null)
challenge = authToken.getChallenge().getBytes();
}
// Get the flags from the client request and mask out unsupported features
int ntlmFlags = type1Msg.getFlags() & NTLM_FLAGS;
// Build a type2 message to send back to the client, containing the challenge
List<TargetInfo> tList = new ArrayList<TargetInfo>();
tList.add(new TargetInfo(NTLM.TargetServer, m_srvName));
Type2NTLMMessage type2Msg = new Type2NTLMMessage();
type2Msg.buildType2(ntlmFlags, m_srvName, challenge, null, tList);
// Store the NTLM logon details, cache the type2 message, and token if using passthru
ntlmDetails = new NTLMLogonDetails();
ntlmDetails.setType2Message( type2Msg);
ntlmDetails.setAuthenticationToken(authToken);
httpSess.setAttribute(NTLM_AUTH_DETAILS, ntlmDetails);
// Debug
if ( logger.isDebugEnabled())
logger.debug('Sending NTLM type2 to client - ' + type2Msg);
// Send back a request for NTLM authentication
byte[] type2Bytes = type2Msg.getBytes();
String ntlmBlob = 'NTLM ' + new String(Base64.encodeBase64(type2Bytes));
resp.setHeader('WWW-Authenticate', ntlmBlob);
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.flushBuffer();
return;
}
}
/**
* Process a type 3 NTLM message
*
* @param type3Msg Type3NTLMMessage
* @param req HttpServletRequest
* @param resp HttpServletResponse
* @param httpSess HttpSession
* @param chain FilterChain
* @exception IOException
* @exception ServletException
*/
private void processType3(Type3NTLMMessage type3Msg, HttpServletRequest req, HttpServletResponse resp,
HttpSession httpSess, FilterChain chain) throws IOException, ServletException
{
// Debug
if ( logger.isDebugEnabled())
logger.debug('Received type3 ' + type3Msg);
// Get the existing NTLM details
NTLMLogonDetails ntlmDetails = null;
User user = null;
if ( httpSess != null)
{
ntlmDetails = (NTLMLogonDetails) httpSess.getAttribute(NTLM_AUTH_DETAILS);
user = (User) httpSess.getAttribute(AuthenticationHelper.AUTHENTICATION_USER);
}
// Get the NTLM logon details
String userName = type3Msg.getUserName();
String workstation = type3Msg.getWorkstation();
String domain = type3Msg.getDomain();
boolean authenticated = false;
boolean useNTLM = true;
// Check if we are using cached details for the authentication
if ( user != null && ntlmDetails != null && ntlmDetails.hasNTLMHashedPassword())
{
// Check if the received NTLM hashed password matches the cached password
byte[] ntlmPwd = type3Msg.getNTLMHash();
byte[] cachedPwd = ntlmDetails.getNTLMHashedPassword();
if ( ntlmPwd != null)
{
if ( ntlmPwd.length == cachedPwd.length)
{
authenticated = true;
for ( int i = 0; i
In reference to http://forums.alfresco.com/en/viewtopic.php?p=36157&sid=733f19dbad2d2f5065ac8b15f2c3ace9
I downloaded the community version last week (December '08) and the bug is still there. It seems that following this article's suggestion of adding a new 'authenticationComponent' bean definition doesn't set any of the bean's service properties. So the actual bean definition should be this:
<bean id='authenticationComponent'
class='org.alfresco.repo.security.authentication.jaas.JAASAuthenticationComponent'>
<property name='realm'>
<value>DEFAULT.REALM</value>
</property>
<property name='jaasConfigEntryName'>
<value>Alfresco</value>
</property>
<property name='tenantService'>
<ref bean='tenantService'/>
</property>
<property name='nodeService'>
<ref bean='nodeService'/>
</property>
<property name='transactionService'>
<ref bean='transactionService'/>
</property>
<property name='personService'>
<ref bean='personService'/>
</property>
</bean>
--Dberansky 16:46, 24 December 2008 (UTC)