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