/* -*- mode: java; c-basic-offset: 4; indent-tabs-mode: nil -*- * * Copyright (C) 2001 ArsDigita Corporation. All Rights Reserved. * * The contents of this file are subject to the ArsDigita Public * License (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.arsdigita.com/ADPL.txt * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * */ package com.arsdigita.auth.http; import com.arsdigita.db.Sequences; import com.arsdigita.kernel.security.*; import com.arsdigita.persistence.DataOperation; import com.arsdigita.persistence.DataQuery; import com.arsdigita.persistence.SessionManager; import com.arsdigita.util.UncheckedWrapperException; import com.arsdigita.web.RedirectSignal; import com.arsdigita.web.Web; import java.math.BigDecimal; import java.net.InetAddress; import java.net.URLEncoder; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.security.PublicKey; import java.util.Date; import java.util.Enumeration; import java.util.Map; import javax.crypto.Cipher; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.login.LoginException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import sun.misc.BASE64Decoder; /** *

* Attempt to authenticate a user against an IIS server for seamless login * from an IE session. *

* *

* This login module is placed after the normal CookieLoginModule * like so: *

* *
 * init com.arsdigita.kernel.security.Initializer {
 *   ...
 *   loginConfig = {
 *     "Request", {
 *          {"com.arsdigita.kernel.security.AdminLoginModule",
 *           "sufficient"},
 *          {"com.arsdigita.kernel.security.RecoveryLoginModule",
 *           "sufficient"},
 *          {"com.arsdigita.kernel.security.CookieLoginModule",
 *           "sufficient"},
 *          {"com.arsdigita.auth.ntlm.HTTPLoginModule",
 *           "requisite"},
 *          {"com.arsdigita.kernel.security.CookieLoginModule",
 *           "optional"}
 *      },
 *   ...
 * 
* *

* You also need an IIS server, running HTTPAuthServlet * (q.v.). *

* *

* When a request is received which is not authenticated, the * {@link #login login} method in this class will be called as part * of the JAAS chain of handlers. The login method works out if the * user is using IE by sniffing their User-Agent. If * so, then it generates a one-off random string called a nonce * and saves this in the database. The nonce is used to prevent * replay attacks, but is otherwise not needed. It then redirects * the user's browser to the IIS server, with the following arguments: *

* *
 * http://iis-server/foo ? nonce=nonce & returnURL=original URL
 * 
* *

* The IIS server runs HTTPAuthServlet. This * basically calls request.getRemoteUser() which does * some proprietary M$ voodoo to fetch the username from the * browser. It takes the username and nonce and signs them with * its private key (see below) and redirects back to us at the * following URL: *

* *
 * original URL & auth=magic number, username, nonce and signature
 * 
* *

* Because the request still doesn't contain a cookie, we * will get this request during the normal course of processing, but * this time the auth parameter will be set. So we * process the parameter, check the signature, and map the IIS/HTTP * username to a CCM User. *

* *

* To see how key managament works and how to see it up, go to * the javadoc for HTTPAuthServlet. *

* * @author Matt Booth, documentation by Richard W.M. Jones * * @see com.arsdigita.auth.ntlm.HTTPAuthServlet */ public class HTTPLoginModule extends MappingLoginModule { private static Logger s_log = Logger.getLogger ( HTTPLoginModule.class ); private static PublicKey s_publicKey = null; static { s_log.debug("Static initalizer starting..."); if (HTTPAuth.getConfig().isActive()) { if (s_log.isDebugEnabled()) { s_log.debug("Loading public key"); } s_publicKey = HTTPAuth.getConfig().getPublicKey(); } else { if (s_log.isInfoEnabled()) { s_log.info("HTTP auth is not active"); } } s_log.debug("Static initalizer finished."); } private static BASE64Decoder s_base64Decoder = new BASE64Decoder(); // The time in seconds until a nonce expires private static final String TTL = "60"; private Subject m_subject; private CallbackHandler m_handler; private Map m_shared; private Map m_options; private BigDecimal m_userID = null; private Boolean m_secure = null; /* This is the decryption cipher. */ private Cipher m_decrypt; public HTTPLoginModule() { m_decrypt = getDecryptionCipher (); } public void initialize( Subject subject, CallbackHandler handler, Map shared, Map options ) { m_subject = subject; m_handler = handler; m_shared = shared; m_options = options; super.initialize( subject, handler, shared, options ); } public boolean login() throws LoginException { if (s_log.isDebugEnabled()) { s_log.debug( "HTTP Login Start" ); } if (m_decrypt == null) { if (s_log.isInfoEnabled()) { s_log.info("No public key available, falling back to default"); } return false; } // Get the request and response objects HttpServletRequest req; HttpServletResponse res; try { HTTPRequestCallback reqCB = new HTTPRequestCallback(); HTTPResponseCallback resCB = new HTTPResponseCallback(); m_handler.handle( new Callback[] { reqCB, resCB } ); req = reqCB.getRequest(); res = resCB.getResponse(); } catch( Exception e ) { throw new UncheckedWrapperException( e ); } // Check address is in range requested. Inet4AddressRange range = HTTPAuth.getConfig().getClientIPRange (); if (range != null) { InetAddress address; String ipaddress = req.getHeader("X-Forwarded-For"); if (ipaddress == null) { ipaddress = req.getRemoteAddr(); if (s_log.isDebugEnabled()) { s_log.debug ("Remote Address = " + ipaddress); } } else { if (s_log.isDebugEnabled()) { s_log.debug ("Proxy forwarded chain is " + ipaddress); } int index = ipaddress.indexOf(','); if (index != -1) { ipaddress = ipaddress.substring(0, index); } if (s_log.isDebugEnabled()) { s_log.debug ("Proxy forwarded client is " + ipaddress); } } try { address = InetAddress.getByName (ipaddress); } catch (UnknownHostException ex) { s_log.warn("Unknown host " + ipaddress, ex); // Abort NTLM auth if can't lookup host // since it could be temporary DNS failure // so falling back on normal auth is nicer // to the user. return false; } catch (SecurityException ex) { throw new UncheckedWrapperException (ex); } if (s_log.isDebugEnabled()) { s_log.debug ("Address = " + address); s_log.debug ("Range = " + range); } if (!range.inRange (address)) { if (s_log.isDebugEnabled()) { s_log.debug ("Address not in range"); } return false; } } // This authentication method only works with IE. If we haven't got IE // just fail immediately String userAgent = req.getHeader( "user-agent" ); boolean isIE = userAgent != null && userAgent.toLowerCase().indexOf( "msie" ) != -1; if( !isIE ) { if (s_log.isDebugEnabled()) { s_log.debug( "User not using IE, falling back to default login" ); } return false; } else { if (s_log.isDebugEnabled()) { s_log.debug( "Attempting HTTP authentication" ); } } // Check if we've got an Auth String String auth = req.getParameter (HTTPAuthServlet.AUTH); if( auth == null ) { // Fetch a new nonce and add it to the db String nonce; try { nonce = Sequences.getNextValue().toString(); DataOperation op = SessionManager.getSession().retrieveDataOperation ( "com.arsdigita.auth.ntlm.AddNonce" ); op.setParameter( "nonce", nonce ); op.setParameter( "expires", new Date(new Date().getTime() + (HTTPAuth.getConfig().getNonceTTL() * 1000l))); op.setParameter( "status", Boolean.FALSE); op.execute(); op.close(); if (s_log.isDebugEnabled()) { s_log.debug( "Added nonce: " + nonce ); } } catch( Exception e ) { throw new UncheckedWrapperException( e ); } String returnURL = "http://" + Web.getConfig().getHost().getName() + req.getRequestURI(); if( req.getQueryString () != null ) { returnURL = returnURL + "?" + req.getQueryString (); } returnURL = URLEncoder.encode( returnURL ); if (s_log.isDebugEnabled()) { s_log.debug( "Return URL: " + returnURL ); } String redirectURL = "http://" + HTTPAuth.getConfig().getServerName() + ":" + HTTPAuth.getConfig().getServerPort() + "/auth/" + "?nonce=" + nonce + "&returnURL=" + returnURL; clearCookie(req, res); throw new RedirectSignal (redirectURL, true); } // Decrypt the authorisation string try { byte[] decAuthBytes = s_base64Decoder.decodeBuffer( auth ); auth = new String( m_decrypt.doFinal( decAuthBytes ) ); } catch( Exception e ) { s_log.warn( "Error checking nonce value: " + e ); e.printStackTrace(); throw new LoginException ( "Invalid signature from authentication server" ); } if( !auth.startsWith( HTTPAuthServlet.MAGIC ) ) { s_log.warn ("Invalid signature from authentication server " + "- no MAGIC (auth was: " + auth + ")"); throw new LoginException ( "Invalid signature from authentication server" ); } auth = auth.substring( HTTPAuthServlet.MAGIC.length() ); if (s_log.isDebugEnabled()) { s_log.debug( "Decrypted Auth: " + auth ); } // Get the nonce and username from the auth string int sep = auth.indexOf( "|" ); String nonce = auth.substring( 0, sep ); String user = auth.substring( sep + 1 ); if (s_log.isDebugEnabled()) { s_log.debug( "Nonce: " + nonce ); s_log.debug( "User: " + user ); } // Check that the nonce we got is valid DataQuery check = SessionManager.getSession().retrieveQuery ( "com.arsdigita.auth.ntlm.CheckNonce" ); check.setParameter( "now", new Date()); check.setParameter( "nonce", nonce ); check.setParameter( "status", Boolean.FALSE); boolean nonceInvalid = check.isEmpty(); check.close(); if( nonceInvalid ) { s_log.warn( "Received invalid nonce: " + nonce ); throw new LoginException ( "Invalid challenge string from authentication server" ); } // Expire the used nonce DataOperation expire = SessionManager.getSession().retrieveDataOperation ( "com.arsdigita.auth.ntlm.ExpireNonce" ); expire.setParameter( "nonce", nonce ); expire.setParameter( "status", Boolean.TRUE); expire.execute(); expire.close(); m_shared.put( PasswordLoginModule.NAME_KEY, user ); // Pull query vars into a HashMap called vars Enumeration vars = req.getParameterNames (); // Redirect again to this URL without the camden nonce. This leaves a // prettier URL and allows the displayed page to be returned from a // cache if appropriate. StringBuffer requestURL = new StringBuffer( req.getRequestURI() ); // got more than just the camden nonce in the query string if (vars.hasMoreElements()) { requestURL.append( '?' ); boolean first = true; while (vars.hasMoreElements()) { String key = (String)vars.nextElement(); String[] vals = req.getParameterValues(key); for (int i = 0 ; i < vals.length ; i++) { // Don't include the nonce in the output if( !HTTPAuthServlet.AUTH.equals(key) ) { if (first) first = false; else requestURL.append( '&' ); requestURL.append(key); requestURL.append( '=' ); requestURL.append(vals[i]); } } } } m_shared.put( RedirectLoginModule.REDIRECT_URL, requestURL.toString() ); if (s_log.isDebugEnabled()) { s_log.debug( "HTTP Login End" ); } return super.login(); } /** * Sets the named cookie to the given value. **/ private void clearCookie(HttpServletRequest req, HttpServletResponse res) throws LoginException { Cookie cookie = new Cookie(isSecure(req) ? UserLoginModule.SECURE_CREDENTIAL_NAME : UserLoginModule.NORMAL_CREDENTIAL_NAME, ""); cookie.setMaxAge(0); cookie.setPath("/"); cookie.setSecure(isSecure(req)); res.addCookie(cookie); } protected final boolean isSecure(HttpServletRequest req) throws LoginException { if (m_secure == null) { m_secure = new Boolean (Util.getSecurityHelper().isSecure(req)); } return m_secure.booleanValue(); } protected BigDecimal getUserID( String ident ) throws LoginException { UserLogin login = UserLogin.findByLogin(ident); if (login == null) { s_log.warn( "No entry for user " + ident ); throw new LoginException( "No entry for user " + ident ); } return login.getUser().getID(); } private Cipher getDecryptionCipher() { // Create the decryption cipher. if (s_publicKey == null) { return null; } Cipher decrypt; try { decrypt = Cipher.getInstance (HTTPAuth.getConfig().getKeyCypher()); decrypt.init( Cipher.DECRYPT_MODE, s_publicKey ); } catch (GeneralSecurityException ex) { throw new UncheckedWrapperException (ex); } return decrypt; } public boolean commit() throws LoginException { if (s_log.isDebugEnabled()) { s_log.debug( "Commit" ); } return super.commit(); } public boolean abort() throws LoginException { if (s_log.isDebugEnabled()) { s_log.debug( "Abort" ); } return super.commit(); } }