001/*
002 * (C) Copyright 2010-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Gagnavarslan ehf
016 *     Thomas Haines
017 */
018package org.nuxeo.ecm.ui.web.auth.digest;
019
020import java.io.IOException;
021import java.io.StringReader;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.regex.Pattern;
027
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030
031import org.apache.commons.codec.binary.Base64;
032import org.apache.commons.codec.digest.DigestUtils;
033import org.apache.commons.csv.CSVFormat;
034import org.apache.commons.csv.CSVParser;
035import org.apache.commons.csv.CSVRecord;
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039
040import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
041import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
042
043/**
044 * Nuxeo Authenticator for HTTP Digest Access Authentication (RFC 2617).
045 */
046public class DigestAuthenticator implements NuxeoAuthenticationPlugin {
047
048    private static final Log log = LogFactory.getLog(DigestAuthenticator.class);
049
050    protected static final String DEFAULT_REALMNAME = "NUXEO";
051
052    protected static final long DEFAULT_NONCE_VALIDITY_SECONDS = 1000;
053
054    protected static final String EQUAL_SEPARATOR = "=";
055
056    protected static final String QUOTE = "\"";
057
058    /*
059     * match the first portion up until an equals sign followed by optional white space of quote chars and ending with
060     * an optional quote char Pattern is a thread-safe class and so can be defined statically Example pair pattern:
061     * username="kirsty"
062     */
063    protected static final Pattern PAIR_ITEM_PATTERN = Pattern.compile("^(.*?)=([\\s\"]*)?(.*)(\")?$");
064
065    protected static final String REALM_NAME_KEY = "RealmName";
066
067    protected static final String BA_HEADER_NAME = "WWW-Authenticate";
068
069    protected String realmName;
070
071    protected long nonceValiditySeconds = DEFAULT_NONCE_VALIDITY_SECONDS;
072
073    protected String accessKey = "key";
074
075    @Override
076    public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
077
078        long expiryTime = System.currentTimeMillis() + (nonceValiditySeconds * 1000);
079        String signature = DigestUtils.md5Hex(expiryTime + ":" + accessKey);
080        String nonce = expiryTime + ":" + signature;
081        String nonceB64 = new String(Base64.encodeBase64(nonce.getBytes()));
082
083        String authenticateHeader = String.format("Digest realm=\"%s\", qop=\"auth\", nonce=\"%s\"", realmName,
084                nonceB64);
085
086        try {
087            httpResponse.addHeader(BA_HEADER_NAME, authenticateHeader);
088            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
089            return Boolean.TRUE;
090        } catch (IOException e) {
091            return Boolean.FALSE;
092        }
093    }
094
095    @Override
096    public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
097            HttpServletResponse httpResponse) {
098
099        String header = httpRequest.getHeader("Authorization");
100        String DIGEST_PREFIX = "digest ";
101        if (StringUtils.isEmpty(header) || !header.toLowerCase().startsWith(DIGEST_PREFIX)) {
102            return null;
103        }
104        Map<String, String> headerMap = splitParameters(header.substring(DIGEST_PREFIX.length()));
105        headerMap.put("httpMethod", httpRequest.getMethod());
106
107        String nonceB64 = headerMap.get("nonce");
108        String nonce = new String(Base64.decodeBase64(nonceB64.getBytes()));
109        String[] nonceTokens = nonce.split(":");
110
111        @SuppressWarnings("unused")
112        long nonceExpiryTime = Long.parseLong(nonceTokens[0]);
113        // @TODO: check expiry time and do something
114
115        String username = headerMap.get("username");
116        String responseDigest = headerMap.get("response");
117        UserIdentificationInfo userIdent = new UserIdentificationInfo(username, responseDigest);
118
119        /*
120         * I have used this property to transfer response parameters to DigestLoginPlugin But loginParameters rewritten
121         * in NuxeoAuthenticationFilter common implementation
122         * @TODO: Fix this or find new way to transfer properties to LoginPlugin
123         */
124        userIdent.setLoginParameters(headerMap);
125        return userIdent;
126
127    }
128
129    @Override
130    public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
131        // @TODO: Use DIGEST authentication for WebDAV and WSS
132        return Boolean.TRUE;
133    }
134
135    @Override
136    public void initPlugin(Map<String, String> parameters) {
137        if (parameters.containsKey(REALM_NAME_KEY)) {
138            realmName = parameters.get(REALM_NAME_KEY);
139        } else {
140            realmName = DEFAULT_REALMNAME;
141        }
142    }
143
144    @Override
145    public List<String> getUnAuthenticatedURLPrefix() {
146        return null;
147    }
148
149    public static Map<String, String> splitParameters(String auth) {
150        Map<String, String> map = new HashMap<>();
151        try (CSVParser reader = new CSVParser(new StringReader(auth), CSVFormat.DEFAULT)) {
152            Iterator<CSVRecord> iterator = reader.iterator();
153            if (iterator.hasNext()) {
154                CSVRecord record = iterator.next();
155                for (String itemPairStr : record) {
156                    itemPairStr = StringUtils.remove(itemPairStr, QUOTE);
157                    String[] parts = itemPairStr.split(EQUAL_SEPARATOR, 2);
158                    if (parts == null) {
159                        continue;
160                    } else {
161                        map.put(parts[0].trim(), parts[1].trim());
162                    }
163                }
164            }
165        } catch (IOException e) {
166            log.error(e.getMessage(), e);
167        }
168        return map;
169    }
170
171}