001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id: JOOoConvertPluginImpl.java 18651 2007-05-13 20:28:53Z sfermigier $
020 */
021
022package org.nuxeo.ecm.platform.ui.web.auth.ntlm;
023
024import java.io.IOException;
025import java.util.List;
026import java.util.Map;
027
028import javax.servlet.ServletException;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031import javax.servlet.http.HttpSession;
032
033import jcifs.Config;
034import jcifs.UniAddress;
035import jcifs.http.NtlmSsp;
036
037import jcifs.smb.NtlmChallenge;
038import jcifs.smb.NtlmPasswordAuthentication;
039import jcifs.smb.SmbAuthException;
040import jcifs.smb.SmbSession;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
044import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
045
046import static jcifs.smb.NtStatus.NT_STATUS_ACCESS_VIOLATION;
047
048public class NTLMAuthenticator implements NuxeoAuthenticationPlugin {
049
050    private static final String JCIFS_PREFIX = "jcifs.";
051
052    public static final String JCIFS_NETBIOS_CACHE_POLICY = "jcifs.netbios.cachePolicy";
053
054    public static final String JCIFS_SMB_CLIENT_SO_TIMEOUT = "jcifs.smb.client.soTimeout";
055
056    public static final String JCIFS_HTTP_LOAD_BALANCE = "jcifs.http.loadBalance";
057
058    public static final String JCIFS_HTTP_DOMAIN_CONTROLLER = "jcifs.http.domainController";
059
060    public static final String JCIFS_SMB_CLIENT_DOMAIN = "jcifs.smb.client.domain";
061
062    public static final boolean FORCE_SESSION_CREATION = true;
063
064    public static final String NTLM_HTTP_AUTH_SESSION_KEY = "NtlmHttpAuth";
065
066    public static final String NTLM_HTTP_CHAL_SESSION_KEY = "NtlmHttpChal";
067
068    protected static String defaultDomain;
069
070    protected static String domainController;
071
072    protected static boolean loadBalance;
073
074    private static final Log log = LogFactory.getLog(NTLMAuthenticator.class);
075
076    @Override
077    public List<String> getUnAuthenticatedURLPrefix() {
078        return null;
079    }
080
081    @Override
082    public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
083
084        log.debug("Handle NTLM login prompt");
085        NtlmPasswordAuthentication ntlm = null;
086        HttpSession httpSession = httpRequest.getSession(FORCE_SESSION_CREATION);
087
088        if (httpSession != null) {
089            ntlm = (NtlmPasswordAuthentication) httpSession.getAttribute(NTLM_HTTP_AUTH_SESSION_KEY);
090        }
091
092        if (httpSession == null || ntlm == null) {
093            log.debug("Sending NTLM Challenge/Response request to browser");
094            httpResponse.setHeader("WWW-Authenticate", "NTLM");
095            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
096            httpResponse.setContentLength(0);
097            try {
098                httpResponse.flushBuffer();
099            } catch (IOException e) {
100                log.error("Error while flushing buffer:" + e.getMessage(), e);
101            }
102            return true;
103        } else {
104            log.debug("No NTLM Prompt done since NTLM Auth was found :" + ntlm.getUsername());
105            return false;
106        }
107    }
108
109    @Override
110    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
111            HttpServletResponse httpResponse) {
112
113        log.debug("NTML handleRetrieveIdentity");
114        NtlmPasswordAuthentication ntlm;
115
116        try {
117            ntlm = negotiate(httpRequest, httpResponse, false);
118        } catch (IOException | ServletException e) {
119            log.error("NTLM negotiation failed : " + e.getMessage(), e);
120            return null;
121        }
122
123        if (ntlm == null) {
124            log.debug("Negotiation returned a null NTLM token");
125            return null;
126        } else {
127            log.debug("Negotiation succeed and returned a NTLM token, creating UserIdentificationInfo");
128            String userName = ntlm.getUsername();
129            log.debug("ntlm.getUsername() = " + userName);
130            if (userName.startsWith(ntlm.getDomain())) {
131                userName = userName.replace(ntlm.getDomain() + "/", "");
132            }
133            log.debug("userName = " + userName);
134            String password = ntlm.getPassword();
135            if (password == null || "".equals(password)) {
136                // we don't get the NTLM password, so we have to trust NTLM auth
137                UserIdentificationInfo userInfo = new UserIdentificationInfo(ntlm.getUsername());
138                return userInfo;
139            } else {
140                return new UserIdentificationInfo(ntlm.getUsername(), ntlm.getPassword());
141            }
142        }
143    }
144
145    @Override
146    public void initPlugin(Map<String, String> parameters) {
147
148        Config.setProperty(JCIFS_SMB_CLIENT_SO_TIMEOUT, "300000");
149        Config.setProperty(JCIFS_NETBIOS_CACHE_POLICY, "1200");
150
151        // init CIFS from parameters
152        for (String name : parameters.keySet()) {
153            if (name.startsWith(JCIFS_PREFIX)) {
154                Config.setProperty(name, parameters.get(name));
155            }
156        }
157
158        // get params from CIFS config
159        defaultDomain = Config.getProperty(JCIFS_SMB_CLIENT_DOMAIN);
160        domainController = Config.getProperty(JCIFS_HTTP_DOMAIN_CONTROLLER);
161        if (domainController == null) {
162            domainController = defaultDomain;
163            loadBalance = Config.getBoolean(JCIFS_HTTP_LOAD_BALANCE, true);
164        }
165    }
166
167    @Override
168    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
169        String useragent = httpRequest.getHeader("User-Agent").toLowerCase();
170
171        // only prompt on windows platform
172
173        if (!useragent.contains("windows")) {
174            log.debug("No NTLM LoginPrompt : User does not use Win32");
175            return false;
176        }
177
178        log.debug("NTLM LoginPrompt Needed");
179        return true;
180    }
181
182    public static NtlmPasswordAuthentication negotiate(HttpServletRequest req, HttpServletResponse resp,
183            boolean skipAuthentication) throws IOException, ServletException {
184        log.debug("NTLM negotiation starts");
185
186        String msg = req.getHeader("Authorization");
187
188        log.debug("NTLM negotiation header = " + msg);
189        NtlmPasswordAuthentication ntlm;
190        if (msg != null && msg.startsWith("NTLM ")) {
191            HttpSession ssn = req.getSession();
192            byte[] challenge;
193
194            UniAddress dc;
195            if (loadBalance) {
196                NtlmChallenge chal = (NtlmChallenge) ssn.getAttribute(NTLM_HTTP_CHAL_SESSION_KEY);
197                if (chal == null) {
198                    chal = SmbSession.getChallengeForDomain();
199                    ssn.setAttribute(NTLM_HTTP_CHAL_SESSION_KEY, chal);
200                }
201                dc = chal.dc;
202                challenge = chal.challenge;
203            } else {
204                dc = UniAddress.getByName(domainController, true);
205                dc = UniAddress.getByName(dc.getHostAddress(), true);
206                challenge = SmbSession.getChallenge(dc);
207            }
208
209            ntlm = NtlmSsp.authenticate(req, resp, challenge);
210            if (ntlm == null) {
211                log.debug("NtlmSsp.authenticate returned null");
212                return null;
213            }
214
215            log.debug("NtlmSsp.authenticate succeed");
216            log.debug("Domain controller is " + dc.getHostName());
217            if (ntlm.getDomain() != null) {
218                log.debug("NtlmSsp.authenticate => domain = " + ntlm.getDomain());
219            } else {
220                log.debug("NtlmSsp.authenticate => null domain");
221            }
222            if (ntlm.getUsername() != null) {
223                log.debug("NtlmSsp.authenticate => userName = " + ntlm.getUsername());
224            } else {
225                log.debug("NtlmSsp.authenticate => userName = null");
226            }
227            if (ntlm.getPassword() != null) {
228                log.debug("NtlmSsp.authenticate => password = " + ntlm.getPassword());
229            } else {
230                log.debug("NtlmSsp.authenticate => password = null");
231            }
232
233            /* negotiation complete, remove the challenge object */
234            ssn.removeAttribute(NTLM_HTTP_CHAL_SESSION_KEY);
235            if (!skipAuthentication) {
236                try {
237                    log.debug("Trying to logon NTLM session on dc " + dc.toString());
238                    SmbSession.logon(dc, ntlm);
239                    log.debug(ntlm + " successfully authenticated against " + dc);
240
241                } catch (SmbAuthException sae) {
242
243                    log.error(ntlm.getName() + ": 0x" + jcifs.util.Hexdump.toHexString(sae.getNtStatus(), 8) + ": "
244                            + sae);
245
246                    if (sae.getNtStatus() == NT_STATUS_ACCESS_VIOLATION) {
247                        /*
248                         * Server challenge no longer valid for externally supplied password hashes.
249                         */
250                        ssn = req.getSession(false);
251                        if (ssn != null) {
252                            ssn.removeAttribute(NTLM_HTTP_AUTH_SESSION_KEY);
253                        }
254                    }
255                    resp.setHeader("WWW-Authenticate", "NTLM");
256                    resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
257                    resp.setContentLength(0);
258                    resp.flushBuffer();
259                    return null;
260                }
261                req.getSession().setAttribute(NTLM_HTTP_AUTH_SESSION_KEY, ntlm);
262            }
263        } else {
264            log.debug("NTLM negotiation header is null");
265            return null;
266        }
267        return ntlm;
268    }
269
270}