Merge pull request #2449 from lachlan-roberts/jetty-9.4.x-1555-wwwAuthenticate-Parsing

Fixes #1555 AuthenticationProtocolHandler unable to parse Digest WWW Header.
This commit is contained in:
Simone Bordet 2018-04-26 00:02:54 +02:00 committed by GitHub
commit ed51c4a9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 252 additions and 88 deletions

View File

@ -20,12 +20,14 @@ package org.eclipse.jetty.client;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.Authentication.HeaderInfo;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.ContentResponse;
@ -35,6 +37,8 @@ import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -42,7 +46,11 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
{
public static final int DEFAULT_MAX_CONTENT_LENGTH = 16*1024;
public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class);
private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+(.*,\\s*)?realm=\"([^\"]*)\"\\s*,?\\s*(.*)", Pattern.CASE_INSENSITIVE);
private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=([^=]+)?");
private static final Pattern TYPE_PATTERN = Pattern.compile("([^\\s]+)(\\s+(.*))?");
private static final Pattern MULTIPLE_CHALLENGE_PATTERN = Pattern.compile("(.*?)\\s*,\\s*([^=\\s,]+(\\s+[^=\\s].*)?)");
private static final Pattern BASE64_PATTERN = Pattern.compile("[\\+\\-\\.\\/\\dA-Z_a-z~]+=*");
private final HttpClient client;
private final int maxContentLength;
@ -74,6 +82,80 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
// Return new instances every time to keep track of the response content
return new AuthenticationListener();
}
protected List<HeaderInfo> getHeaderInfo(String value) throws IllegalArgumentException
{
String header = value;
List<HeaderInfo> headerInfos = new ArrayList<>();
while(true)
{
Matcher m = MULTIPLE_CHALLENGE_PATTERN.matcher(header);
if (m.matches())
{
headerInfos.add(newHeaderInfo(m.group(1)));
header = m.group(2);
}
else
{
headerInfos.add(newHeaderInfo(header));
break;
}
}
return headerInfos;
}
protected HeaderInfo newHeaderInfo(String value) throws IllegalArgumentException
{
String type;
Map<String,String> params = new HashMap<>();
Matcher m = TYPE_PATTERN.matcher(value);
if (m.matches())
{
type = m.group(1);
if (m.group(2) != null)
params = parseParameters(m.group(3));
}
else
{
throw new IllegalArgumentException("Invalid Authentication Format");
}
return new HeaderInfo(getAuthorizationHeader(), type, params);
}
protected Map<String, String> parseParameters(String wwwAuthenticate) throws IllegalArgumentException
{
Map<String, String> result = new HashMap<>();
Matcher b64 = BASE64_PATTERN.matcher(wwwAuthenticate);
if (b64.matches())
{
result.put("base64", wwwAuthenticate);
return result;
}
QuotedCSV parts = new QuotedCSV(false, wwwAuthenticate);
for (String part : parts)
{
Matcher params = PARAM_PATTERN.matcher(part);
if (params.matches())
{
String name = StringUtil.asciiToLowerCase(params.group(1));
String value = (params.group(2)==null) ? "" : params.group(2);
result.put(name, value);
}
else
{
throw new IllegalArgumentException("Invalid Authentication Format");
}
}
return result;
}
private class AuthenticationListener extends BufferingResponseListener
{
@ -234,25 +316,17 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
{
// TODO: these should be ordered by strength
List<Authentication.HeaderInfo> result = new ArrayList<>();
List<String> values = Collections.list(response.getHeaders().getValues(header.asString()));
List<String> values = response.getHeaders().getValuesList(header);
for (String value : values)
{
Matcher matcher = AUTHENTICATE_PATTERN.matcher(value);
if (matcher.matches())
try
{
String type = matcher.group(1);
String realm = matcher.group(3);
String beforeRealm = matcher.group(2);
String afterRealm = matcher.group(4);
String params;
if (beforeRealm != null)
params = beforeRealm + afterRealm;
else
params = afterRealm;
Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader());
result.add(headerInfo);
result.addAll(getHeaderInfo(value));
}
catch(IllegalArgumentException e)
{
if (LOG.isDebugEnabled())
LOG.debug("Failed to parse authentication header", e);
}
}
return result;

View File

@ -19,9 +19,11 @@
package org.eclipse.jetty.client.api;
import java.net.URI;
import java.util.Map;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.StringUtil;
/**
* {@link Authentication} represents a mechanism to authenticate requests for protected resources.
@ -76,19 +78,18 @@ public interface Authentication
*/
public static class HeaderInfo
{
private final String type;
private final String realm;
private final String params;
private final HttpHeader header;
private final String type;
private final Map<String,String> params;
public HeaderInfo(String type, String realm, String params, HttpHeader header)
public HeaderInfo(HttpHeader header, String type, Map<String,String> params) throws IllegalArgumentException
{
this.type = type;
this.realm = realm;
this.params = params;
this.header = header;
this.type = type;
this.params = params;
}
/**
* @return the authentication type (for example "Basic" or "Digest")
*/
@ -98,20 +99,36 @@ public interface Authentication
}
/**
* @return the realm name
* @return the realm name or null if there is no realm parameter
*/
public String getRealm()
{
return realm;
return params.get("realm");
}
/**
* @return the base64 content as a string if it exists otherwise null
*/
public String getBase64()
{
return params.get("base64");
}
/**
* @return additional authentication parameters
*/
public String getParameters()
public Map<String, String> getParameters()
{
return params;
}
/**
* @return specified authentication parameter or null if does not exist
*/
public String getParameter(String paramName)
{
return params.get(StringUtil.asciiToLowerCase(paramName));
}
/**
* @return the {@code Authorization} (or {@code Proxy-Authorization}) header

View File

@ -22,15 +22,11 @@ import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.AuthenticationStore;
@ -40,6 +36,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
/**
* Implementation of the HTTP "Digest" authentication defined in RFC 2617.
@ -50,8 +47,6 @@ import org.eclipse.jetty.util.TypeUtil;
*/
public class DigestAuthentication extends AbstractAuthentication
{
private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
private final String user;
private final String password;
@ -74,10 +69,21 @@ public class DigestAuthentication extends AbstractAuthentication
return "Digest";
}
@Override
public boolean matches(String type, URI uri, String realm)
{
// digest authenication requires a realm
if (realm == null)
return false;
return super.matches(type,uri,realm);
}
@Override
public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context)
{
Map<String, String> params = parseParameters(headerInfo.getParameters());
Map<String, String> params = headerInfo.getParameters();
String nonce = params.get("nonce");
if (nonce == null || nonce.length() == 0)
return null;
@ -105,58 +111,6 @@ public class DigestAuthentication extends AbstractAuthentication
return new DigestResult(headerInfo.getHeader(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque);
}
private Map<String, String> parseParameters(String wwwAuthenticate)
{
Map<String, String> result = new HashMap<>();
List<String> parts = splitParams(wwwAuthenticate);
for (String part : parts)
{
Matcher matcher = PARAM_PATTERN.matcher(part);
if (matcher.matches())
{
String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
String value = matcher.group(2).trim();
if (value.startsWith("\"") && value.endsWith("\""))
value = value.substring(1, value.length() - 1);
result.put(name, value);
}
}
return result;
}
private List<String> splitParams(String paramString)
{
List<String> result = new ArrayList<>();
int start = 0;
for (int i = 0; i < paramString.length(); ++i)
{
int quotes = 0;
char ch = paramString.charAt(i);
switch (ch)
{
case '\\':
++i;
break;
case '"':
++quotes;
break;
case ',':
if (quotes % 2 == 0)
{
String element = paramString.substring(start, i).trim();
if (element.length() > 0)
result.add(element);
start = i + 1;
}
break;
default:
break;
}
}
result.add(paramString.substring(start, paramString.length()).trim());
return result;
}
private MessageDigest getMessageDigest(String algorithm)
{
try

View File

@ -23,6 +23,7 @@ import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -43,6 +44,7 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Response.Listener;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.api.Authentication.HeaderInfo;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.DeferredContentProvider;
import org.eclipse.jetty.client.util.DigestAuthentication;
@ -63,6 +65,7 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
@ -613,4 +616,120 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
};
}
}
@Test
public void testTestHeaderInfoParsing() {
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
HeaderInfo headerInfo = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\"auth\", nonce=\"1523430383\"").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfo.getParameter("qop").equals("auth"));
Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat"));
Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383"));
headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", realm=\"thermostat\", nonce=\"1523430383\"").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfo.getParameter("qop").equals("auth"));
Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat"));
Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383"));
headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", nonce=\"1523430383\", realm=\"thermostat\"").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfo.getParameter("qop").equals("auth"));
Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat"));
Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383"));
headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", nonce=\"1523430383\"").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfo.getParameter("qop").equals("auth"));
Assert.assertTrue(headerInfo.getParameter("realm") == null);
Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383"));
// test multiple authentications
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest qop=\"auth\", realm=\"thermostat\", nonce=\"1523430383\", "
+ "Digest realm=\"thermostat2\", qop=\"auth2\", nonce=\"4522530354\", "
+ "Digest qop=\"auth3\", nonce=\"9523570528\", realm=\"thermostat3\", "
+ "Digest qop=\"auth4\", nonce=\"3526435321\"");
Assert.assertTrue(headerInfoList.get(0).getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfoList.get(0).getParameter("qop").equals("auth"));
Assert.assertTrue(headerInfoList.get(0).getParameter("realm").equals("thermostat"));
Assert.assertTrue(headerInfoList.get(0).getParameter("nonce").equals("1523430383"));
Assert.assertTrue(headerInfoList.get(1).getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfoList.get(1).getParameter("qop").equals("auth2"));
Assert.assertTrue(headerInfoList.get(1).getParameter("realm").equals("thermostat2"));
Assert.assertTrue(headerInfoList.get(1).getParameter("nonce").equals("4522530354"));
Assert.assertTrue(headerInfoList.get(2).getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfoList.get(2).getParameter("qop").equals("auth3"));
Assert.assertTrue(headerInfoList.get(2).getParameter("realm").equals("thermostat3"));
Assert.assertTrue(headerInfoList.get(2).getParameter("nonce").equals("9523570528"));
Assert.assertTrue(headerInfoList.get(3).getType().equalsIgnoreCase("Digest"));
Assert.assertTrue(headerInfoList.get(3).getParameter("qop").equals("auth4"));
Assert.assertTrue(headerInfoList.get(3).getParameter("realm") == null);
Assert.assertTrue(headerInfoList.get(3).getParameter("nonce").equals("3526435321"));
List<HeaderInfo> headerInfos = aph.getHeaderInfo("Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"");
Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Newauth"));
Assert.assertTrue(headerInfos.get(0).getParameter("realm").equals("apps"));
Assert.assertTrue(headerInfos.get(0).getParameter("type").equals("1"));
Assert.assertThat(headerInfos.get(0).getParameter("title"), Matchers.equalTo("Login to \"apps\""));
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Basic"));
Assert.assertTrue(headerInfos.get(1).getParameter("realm").equals("simple"));
}
@Test
public void testTestHeaderInfoParsingUnusualCases() {
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
HeaderInfo headerInfo = aph.getHeaderInfo("Scheme").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme"));
Assert.assertTrue(headerInfo.getParameter("realm") == null);
List<HeaderInfo> headerInfos = aph.getHeaderInfo("Scheme1 , Scheme2 , Scheme3");
Assert.assertEquals(3, headerInfos.size());
Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme1"));
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Scheme2"));
Assert.assertTrue(headerInfos.get(2).getType().equalsIgnoreCase("Scheme3"));
headerInfo = aph.getHeaderInfo("Scheme name=\"value\", other=\"value2\"").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme"));
Assert.assertTrue(headerInfo.getParameter("name").equals("value"));
Assert.assertTrue(headerInfo.getParameter("other").equals("value2"));
headerInfo = aph.getHeaderInfo("Scheme name = value , other = \"value2\" ").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme"));
Assert.assertTrue(headerInfo.getParameter("name").equals("value"));
Assert.assertTrue(headerInfo.getParameter("other").equals("value2"));
headerInfos = aph.getHeaderInfo("Scheme name=value, Scheme2 name=value2");
Assert.assertEquals(headerInfos.size(), 2);
Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme"));
Assert.assertTrue(headerInfos.get(0).getParameter("nAmE").equals("value"));
Assert.assertThat(headerInfos.get(1).getType(), Matchers.equalToIgnoringCase("Scheme2"));
Assert.assertTrue(headerInfos.get(1).getParameter("nAmE").equals("value2"));
headerInfos = aph.getHeaderInfo("Scheme , ,, ,, name=value, Scheme2 name=value2");
Assert.assertEquals(headerInfos.size(), 2);
Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme"));
Assert.assertTrue(headerInfos.get(0).getParameter("name").equals("value"));
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Scheme2"));
Assert.assertTrue(headerInfos.get(1).getParameter("name").equals("value2"));
//Negotiate with base64 Content
headerInfo = aph.getHeaderInfo("Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAFAs4OAAAADw==").get(0);
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Negotiate"));
Assert.assertTrue(headerInfo.getBase64().equals("TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAFAs4OAAAADw=="));
headerInfos = aph.getHeaderInfo("Negotiate TlRMTVNTUAABAAAAAAAAAFAs4OAAAADw==, "
+ "Negotiate YIIJvwYGKwYBBQUCoIIJszCCCa+gJDAi=");
Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Negotiate"));
Assert.assertTrue(headerInfos.get(0).getBase64().equals("TlRMTVNTUAABAAAAAAAAAFAs4OAAAADw=="));
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Negotiate"));
Assert.assertTrue(headerInfos.get(1).getBase64().equals("YIIJvwYGKwYBBQUCoIIJszCCCa+gJDAi="));
}
}