Index: jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml =================================================================== --- jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml (revision 936184) +++ jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml (revision ) @@ -204,6 +204,10 @@ The webdav servlet that connects HTTP request to the repository. org.apache.jackrabbit.j2ee.SimpleWebdavServlet + resource-path-prefix Index: jackrabbit-core/src/test/resources/passwords.properties =================================================================== --- jackrabbit-core/src/test/resources/passwords.properties (revision ) +++ jackrabbit-core/src/test/resources/passwords.properties (revision ) @@ -0,0 +1,2 @@ +#Digest of fakeuser:jackrabbit:fakepassword +fakeuser=5d03107efa7fc691c6b34271fc28004c \ No newline at end of file Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.java (revision 961487) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.java (revision ) @@ -31,6 +31,7 @@ suite.addTestSuite(SimpleCredentialsAuthenticationTest.class); suite.addTestSuite(CryptedSimpleCredentialsTest.class); suite.addTestSuite(LoginModuleTest.class); + suite.addTestSuite(DigestAuthenticationTest.class); return suite; } Index: jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DigestWebdavServlet.java =================================================================== --- jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DigestWebdavServlet.java (revision ) +++ jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DigestWebdavServlet.java (revision ) @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.jackrabbit.j2ee; + +import org.apache.jackrabbit.server.CredentialsProvider; +import org.apache.jackrabbit.server.DigestCredentialsProvider; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.WebdavResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Extension of the standard WebDAV servlet to support HTTP Digest authentication. + */ +public class DigestWebdavServlet extends SimpleWebdavServlet { + + private static final long serialVersionUID = 7796841861774352679L; + + private static final Logger log = LoggerFactory.getLogger(DigestWebdavServlet.class); + + private static final String key = "jackrabbit"; + + private static final String realmName = "jackrabbit"; + + @Override + protected void sendUnauthorized(WebdavRequest request, + WebdavResponse response, DavException error) throws IOException { + + String nOnce = generateNOnce(request); + + String header = "Digest realm=\"" + realmName + "\", " + + "qop=\"auth\", nonce=\"" + nOnce + "\", " + "opaque=\"" + + Text.md5(nOnce) + "\""; + + if (log.isDebugEnabled()) { + log.debug("WWW-Authenticate " + header); + } + response.setHeader("WWW-Authenticate", header); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Override + protected CredentialsProvider getCredentialsProvider() { + return new DigestCredentialsProvider(); + } + + protected String generateNOnce(HttpServletRequest request) { + + long currentTime = System.currentTimeMillis(); + + String nOnceValue = request.getRemoteAddr() + ":" + + currentTime + ":" + key; + + return Text.md5(nOnceValue); + } + +} Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/DigestLoginModule.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/DigestLoginModule.java (revision ) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/DigestLoginModule.java (revision ) @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.jackrabbit.core.security.simple; + +import org.apache.jackrabbit.core.security.authentication.Authentication; +import org.apache.jackrabbit.core.security.authentication.DigestAuthentication; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.security.Principal; +import java.security.acl.Group; +import java.util.Map; + +/** + * Login module implementation to support HTTP Digest authentication + */ +public class DigestLoginModule extends SimpleLoginModule { + + private static final Logger log = LoggerFactory + .getLogger(DigestLoginModule.class); + + private static final String PASSWORDS_FILE_OPTION = "passwordsFile"; + + private String passwordsFile = ""; + + @Override + protected void doInit(CallbackHandler callbackHandler, Session session, Map options) + throws LoginException { + super.doInit(callbackHandler, session, options); + + if (options.containsKey(PASSWORDS_FILE_OPTION)) { + passwordsFile = (String) options.get(PASSWORDS_FILE_OPTION); + } else { + throw new LoginException("The mandatory parameter '" + PASSWORDS_FILE_OPTION + + "' is missing."); + } + + } + + @Override + protected Authentication getAuthentication(Principal principal, + Credentials creds) throws RepositoryException { + if (principal instanceof Group) { + return null; + } + try { + return new DigestAuthentication(new FileInputStream(passwordsFile)); + } catch (FileNotFoundException e) { + throw new RepositoryException("The file '" + passwordsFile + "' could not be found.",e); + } + } + + @Override + protected boolean supportsCredentials(Credentials creds) { + return creds instanceof SimpleCredentials; + } + + /* + @Override + protected String getUserID(Credentials credentials) { + String userId = ((SimpleCredentials) credentials).getUserID(); + if (userId == null) { + return super.getUserID(credentials); + } + return userId; + } + */ + + /** + * Generated the digest of the sequence username:realm:password. + * @param username The username + * @param realm The realm + * @param password The clear-text password + * @return Digest of the parameters + */ + public static String digest(String username, String realm, String password) { + return Text.md5(username + ":" + realm + ":" + password); + } + + public static void main(String args[]) { + if (args.length != 3) { + System.out.println("Usage: " + DigestLoginModule.class.getName() + + " "); + } else { + System.out.println(args[0] + "=" + digest(args[0], args[1], args[2])); + } + } + + +} Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DigestAuthenticationTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DigestAuthenticationTest.java (revision ) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DigestAuthenticationTest.java (revision ) @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.jackrabbit.core.security.authentication; + +import org.apache.jackrabbit.core.security.simple.DigestLoginModule; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.SimpleCredentials; +import java.io.InputStream; + +public class DigestAuthenticationTest extends AbstractJCRTest { + + private Authentication authentication; + + @Override + protected void setUp() throws Exception { + super.setUp(); + InputStream is = getClass().getResourceAsStream("/passwords.properties"); + authentication = new DigestAuthentication(is); + } + + public void testAuthenticationFailed() throws Exception { + String username = "fakeuser"; + String realm = "jackrabbit"; + String password = "WRONGPASSWORD"; + + String nonce = "nonce"; + String nc = "nc"; + String cnonce = "cnonce"; + String qop = "qop"; + String md5a2 = "md5a2"; + + String passwordDigest = DigestLoginModule.digest(username, realm, password); + String clientDigest = Text.md5(passwordDigest + ":" + nonce + ":" + nc + + ":" + cnonce + ":" + qop + ":" + md5a2); + + SimpleCredentials cred = new SimpleCredentials(username, clientDigest.toCharArray()); + cred.setAttribute("nonce", nonce); + cred.setAttribute("nc", nc); + cred.setAttribute("cnonce", cnonce); + cred.setAttribute("qop", qop); + cred.setAttribute("realm", realm); + cred.setAttribute("md5a2", md5a2); + + assertFalse(authentication.authenticate(cred)); + + } + + public void testSuccessfulAuthentication() throws Exception { + String username = "fakeuser"; + String realm = "jackrabbit"; + String password = "fakepassword"; + + String nonce = "nonce"; + String nc = "nc"; + String cnonce = "cnonce"; + String qop = "qop"; + String md5a2 = "md5a2"; + + String passwordDigest = DigestLoginModule.digest(username, realm, password); + String clientDigest = Text.md5(passwordDigest + ":" + nonce + ":" + nc + + ":" + cnonce + ":" + qop + ":" + md5a2); + + SimpleCredentials cred = new SimpleCredentials(username, clientDigest.toCharArray()); + cred.setAttribute("nonce", nonce); + cred.setAttribute("nc", nc); + cred.setAttribute("cnonce", cnonce); + cred.setAttribute("qop", qop); + cred.setAttribute("realm", realm); + cred.setAttribute("md5a2", md5a2); + + assertTrue(authentication.authenticate(cred)); + } + +} Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DigestAuthentication.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DigestAuthentication.java (revision ) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DigestAuthentication.java (revision ) @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.jackrabbit.core.security.authentication; + +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Implementation of HTTP Digest authentication. + * This code is based on Tomcat's implementation defined on + * org.apache.catalina.authenticator.DigestAuthenticator. + */ +public class DigestAuthentication implements Authentication { + + + private static final Logger log = LoggerFactory + .getLogger(DigestAuthentication.class); + + private Properties passwords; + + public DigestAuthentication(InputStream is) { + passwords = new Properties(); + try { + passwords.load(is); + } catch (IOException e) { + log.error("The passwords file could not be read.", e); + } + } + + public boolean canHandle(Credentials credentials) { + return (credentials != null && + credentials instanceof SimpleCredentials && + ((SimpleCredentials) credentials).getAttribute("clientdigest") != null + + ); + } + + public boolean authenticate(Credentials credentials) + throws RepositoryException { + SimpleCredentials dc = (SimpleCredentials) credentials; + + return authenticate( + dc.getUserID(), + new String(dc.getPassword()), + dc.getAttribute("nonce").toString(), + dc.getAttribute("nc").toString(), + dc.getAttribute("cnonce").toString(), + dc.getAttribute("qop").toString(), + dc.getAttribute("realm").toString(), + dc.getAttribute("md5a2").toString()); + } + + + public boolean authenticate(String username, String clientDigest, + String nOnce, String nc, String cnonce, String qop, + String realm, String md5a2) { + + String md5a1 = getDigestedPassword(username); + + + String serverDigestValue = md5a1 + ":" + nOnce + ":" + nc + ":" + + cnonce + ":" + qop + ":" + md5a2; + + String serverDigest = Text.md5(serverDigestValue); + + if (log.isDebugEnabled()) { + log.debug("Digest : " + clientDigest + " Username:" + username + + " nOnce:" + nOnce + + " nc:" + nc + " cnonce:" + cnonce + " qop:" + qop + + " realm:" + realm + "md5a2:" + md5a2 + + " Server digest:" + serverDigest); + } + + return (serverDigest.equals(clientDigest)); + } + + protected String getDigestedPassword(String user) { + return this.passwords.getProperty(user); + } + +} Index: jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/DigestCredentialsProvider.java =================================================================== --- jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/DigestCredentialsProvider.java (revision ) +++ jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/DigestCredentialsProvider.java (revision ) @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.apache.jackrabbit.server; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.SimpleCredentials; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Class implements a credentials provider that extracts the credentials + * from the 'WWW-Authenticate' header and only supports 'Digest' authentication. + * This code is based on Tomcat's implementation defined on + * org.apache.catalina.authenticator.DigestAuthenticator. + */ +public class DigestCredentialsProvider implements CredentialsProvider { + + private static final Logger log = LoggerFactory + .getLogger(DigestCredentialsProvider.class); + + public Credentials getCredentials(HttpServletRequest request) + throws LoginException, ServletException { + String authorization = request.getHeader("authorization"); + if (authorization == null || authorization.isEmpty()) { + // User is not logged in, prompt for credentials + throw new LoginException(); + } + if (log.isDebugEnabled()) { + log.debug("authorization: " + authorization); + } + return createDigestCredentials(request, authorization); + } + + protected SimpleCredentials createDigestCredentials( + HttpServletRequest request, String authorization) { + if (authorization == null) + return (null); + if (!authorization.startsWith("Digest ")) + return (null); + authorization = authorization.substring(7).trim(); + + String[] tokens = authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)"); + + String userName = null; + String realmName = null; + String nOnce = null; + String nc = null; + String cnonce = null; + String qop = null; + String uri = null; + String response = null; + String method = request.getMethod(); + + for(String currentToken : tokens) { + if (currentToken.length() == 0) + continue; + + int equalSign = currentToken.indexOf('='); + if (equalSign < 0) + return null; + String currentTokenName = currentToken.substring(0, equalSign) + .trim(); + String currentTokenValue = currentToken.substring(equalSign + 1) + .trim(); + if ("username".equals(currentTokenName)) + userName = removeQuotes(currentTokenValue); + if ("realm".equals(currentTokenName)) + realmName = removeQuotes(currentTokenValue, true); + if ("nonce".equals(currentTokenName)) + nOnce = removeQuotes(currentTokenValue); + if ("nc".equals(currentTokenName)) + nc = removeQuotes(currentTokenValue); + if ("cnonce".equals(currentTokenName)) + cnonce = removeQuotes(currentTokenValue); + if ("qop".equals(currentTokenName)) + qop = removeQuotes(currentTokenValue); + if ("uri".equals(currentTokenName)) + uri = removeQuotes(currentTokenValue); + if ("response".equals(currentTokenName)) + response = removeQuotes(currentTokenValue); + } + + if ((userName == null) || (realmName == null) || (nOnce == null) + || (uri == null) || (response == null)) + return null; + + // Second MD5 digest used to calculate the digest : + // MD5(Method + ":" + uri) + String a2 = method + ":" + uri; + String md5a2 = Text.md5(a2); + + SimpleCredentials creds = new SimpleCredentials(userName, response.toCharArray()); + creds.setAttribute("nonce", nOnce); + creds.setAttribute("nc", nc); + creds.setAttribute("cnonce", cnonce); + creds.setAttribute("qop", qop); + creds.setAttribute("realm", realmName); + creds.setAttribute("md5a2", md5a2); + return creds; + + } + + /** + * Removes the quotes on a string. RFC2617 states quotes are optional for + * all parameters except realm. + */ + protected static String removeQuotes(String quotedString, + boolean quotesRequired) { + //support both quoted and non-quoted + if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && + !quotesRequired) { + return quotedString; + } else if (quotedString.length() > 2) { + return quotedString.substring(1, quotedString.length() - 1); + } else { + return ""; + } + } + + /** + * Removes the quotes on a string. + */ + protected static String removeQuotes(String quotedString) { + return removeQuotes(quotedString, false); + } + +}