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}