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}