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}