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