/*
* (C) Copyright 2010-2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Gagnavarslan ehf
* Thomas Haines
*/
package org.nuxeo.ecm.ui.web.auth.digest;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.api.login.UserIdentificationInfo;
import org.nuxeo.ecm.platform.ui.web.auth.interfaces.NuxeoAuthenticationPlugin;
/**
* Nuxeo Authenticator for HTTP Digest Access Authentication (RFC 2617).
*/
public class DigestAuthenticator implements NuxeoAuthenticationPlugin {
private static final Log log = LogFactory.getLog(DigestAuthenticator.class);
protected static final String DEFAULT_REALMNAME = "NUXEO";
protected static final long DEFAULT_NONCE_VALIDITY_SECONDS = 1000;
protected static final String EQUAL_SEPARATOR = "=";
protected static final String QUOTE = "\"";
/*
* match the first portion up until an equals sign followed by optional white space of quote chars and ending with
* an optional quote char Pattern is a thread-safe class and so can be defined statically Example pair pattern:
* username="kirsty"
*/
protected static final Pattern PAIR_ITEM_PATTERN = Pattern.compile("^(.*?)=([\\s\"]*)?(.*)(\")?$");
protected static final String REALM_NAME_KEY = "RealmName";
protected static final String BA_HEADER_NAME = "WWW-Authenticate";
protected String realmName;
protected long nonceValiditySeconds = DEFAULT_NONCE_VALIDITY_SECONDS;
protected String accessKey = "key";
@Override
public Boolean handleLoginPrompt(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String baseURL) {
long expiryTime = System.currentTimeMillis() + (nonceValiditySeconds * 1000);
String signature = DigestUtils.md5Hex(expiryTime + ":" + accessKey);
String nonce = expiryTime + ":" + signature;
String nonceB64 = new String(Base64.encodeBase64(nonce.getBytes()));
String authenticateHeader = String.format("Digest realm=\"%s\", qop=\"auth\", nonce=\"%s\"", realmName,
nonceB64);
try {
httpResponse.addHeader(BA_HEADER_NAME, authenticateHeader);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return Boolean.TRUE;
} catch (IOException e) {
return Boolean.FALSE;
}
}
@Override
public UserIdentificationInfo handleRetrieveIdentity(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
String header = httpRequest.getHeader("Authorization");
String DIGEST_PREFIX = "digest ";
if (StringUtils.isEmpty(header) || !header.toLowerCase().startsWith(DIGEST_PREFIX)) {
return null;
}
Map<String, String> headerMap = splitParameters(header.substring(DIGEST_PREFIX.length()));
headerMap.put("httpMethod", httpRequest.getMethod());
String nonceB64 = headerMap.get("nonce");
String nonce = new String(Base64.decodeBase64(nonceB64.getBytes()));
String[] nonceTokens = nonce.split(":");
@SuppressWarnings("unused")
long nonceExpiryTime = Long.parseLong(nonceTokens[0]);
// @TODO: check expiry time and do something
String username = headerMap.get("username");
String responseDigest = headerMap.get("response");
UserIdentificationInfo userIdent = new UserIdentificationInfo(username, responseDigest);
/*
* I have used this property to transfer response parameters to DigestLoginPlugin But loginParameters rewritten
* in NuxeoAuthenticationFilter common implementation
* @TODO: Fix this or find new way to transfer properties to LoginPlugin
*/
userIdent.setLoginParameters(headerMap);
return userIdent;
}
@Override
public Boolean needLoginPrompt(HttpServletRequest httpRequest) {
// @TODO: Use DIGEST authentication for WebDAV and WSS
return Boolean.TRUE;
}
@Override
public void initPlugin(Map<String, String> parameters) {
if (parameters.containsKey(REALM_NAME_KEY)) {
realmName = parameters.get(REALM_NAME_KEY);
} else {
realmName = DEFAULT_REALMNAME;
}
}
@Override
public List<String> getUnAuthenticatedURLPrefix() {
return null;
}
public static Map<String, String> splitParameters(String auth) {
Map<String, String> map = new HashMap<>();
try (CSVParser reader = new CSVParser(new StringReader(auth), CSVFormat.DEFAULT)) {
Iterator<CSVRecord> iterator = reader.iterator();
if (iterator.hasNext()) {
CSVRecord record = iterator.next();
for (String itemPairStr : record) {
itemPairStr = StringUtils.remove(itemPairStr, QUOTE);
String[] parts = itemPairStr.split(EQUAL_SEPARATOR, 2);
if (parts == null) {
continue;
} else {
map.put(parts[0].trim(), parts[1].trim());
}
}
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return map;
}
}