Issue #2739 - AuthenticationProtocolHandler Multiple Challenge Pattern
Splitting elements into list using QuotedCSV and processing with state machine instead of using regex to split into multiple challenges. Signed-off-by: lachan-roberts <lachlan@webtide.com>
This commit is contained in:
parent
0ba1d9b5a5
commit
58f2b8f360
|
@ -38,7 +38,6 @@ 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;
|
||||
|
||||
|
@ -47,10 +46,10 @@ 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 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 enum State { AUTH_SCHEME, TOKEN68, AUTH_PARAMS, CHALLENGE_END }
|
||||
private static final Pattern AUTH_SCHEME = Pattern.compile("([!#$%&'*+\\-.^_`|~0-9A-Za-z]+)(?:\\s+(.*))?");
|
||||
private static final Pattern AUTH_PARAM = Pattern.compile("([!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:\"(.*)\"|(.*))");
|
||||
private static final Pattern TOKEN_68 = Pattern.compile("([a-zA-Z0-9\\-._~+\\/]+=*)");
|
||||
|
||||
private final HttpClient client;
|
||||
private final int maxContentLength;
|
||||
|
@ -85,77 +84,106 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler
|
|||
|
||||
|
||||
|
||||
protected List<HeaderInfo> getHeaderInfo(String value) throws IllegalArgumentException
|
||||
protected List<HeaderInfo> getHeaderInfo(String header) throws IllegalArgumentException
|
||||
{
|
||||
String header = value;
|
||||
List<HeaderInfo> headerInfos = new ArrayList<>();
|
||||
|
||||
List<HeaderInfo> headerInfos = new ArrayList<>();
|
||||
List<String> values = new QuotedCSV(true, header).getValues();
|
||||
|
||||
Matcher m;
|
||||
String authScheme = null;
|
||||
Map<String,String> authParams = new HashMap<>();
|
||||
|
||||
State state = State.AUTH_SCHEME;
|
||||
|
||||
int index = 0;
|
||||
String value = values.get(index);
|
||||
|
||||
Loop:
|
||||
while(true)
|
||||
{
|
||||
Matcher m = MULTIPLE_CHALLENGE_PATTERN.matcher(header);
|
||||
if (m.matches())
|
||||
switch(state)
|
||||
{
|
||||
headerInfos.add(newHeaderInfo(m.group(1)));
|
||||
header = m.group(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
headerInfos.add(newHeaderInfo(header));
|
||||
break;
|
||||
case AUTH_SCHEME:
|
||||
|
||||
m = AUTH_SCHEME.matcher(value);
|
||||
if(m.matches())
|
||||
{
|
||||
authScheme = m.group(1);
|
||||
value = m.group(2);
|
||||
state = State.AUTH_PARAMS;
|
||||
|
||||
if(value==null)
|
||||
break;
|
||||
|
||||
m = TOKEN_68.matcher(value);
|
||||
if(m.matches())
|
||||
{
|
||||
value = m.group(1);
|
||||
state = State.TOKEN68;
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid auth-scheme");
|
||||
|
||||
|
||||
case TOKEN68:
|
||||
authParams.put("base64", value);
|
||||
state = State.CHALLENGE_END;
|
||||
break;
|
||||
|
||||
|
||||
case AUTH_PARAMS:
|
||||
if(value == null)
|
||||
{
|
||||
state = State.CHALLENGE_END;
|
||||
continue;
|
||||
}
|
||||
|
||||
m = AUTH_PARAM.matcher(value);
|
||||
if(m.matches())
|
||||
{
|
||||
String paramVal = (m.group(2)!=null) ? m.group(2) : m.group(3);
|
||||
authParams.put(m.group(1), paramVal);
|
||||
break;
|
||||
}
|
||||
|
||||
m = AUTH_SCHEME.matcher(value);
|
||||
if(m.matches())
|
||||
{
|
||||
state = State.CHALLENGE_END;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid auth-param");
|
||||
|
||||
|
||||
case CHALLENGE_END:
|
||||
headerInfos.add(new HeaderInfo(getAuthorizationHeader(), authScheme, authParams));
|
||||
authScheme = null;
|
||||
authParams = new HashMap<>();
|
||||
state = State.AUTH_SCHEME;
|
||||
|
||||
if(value==null)
|
||||
break Loop;
|
||||
|
||||
continue;
|
||||
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Invalid state");
|
||||
}
|
||||
|
||||
|
||||
value = (++index < values.size()) ? values.get(index) : null;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
|
|
@ -31,12 +31,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.Authentication.HeaderInfo;
|
||||
import org.eclipse.jetty.client.api.AuthenticationStore;
|
||||
import org.eclipse.jetty.client.api.ContentProvider;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
|
@ -44,7 +44,6 @@ 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;
|
||||
|
@ -676,7 +675,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
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.assertThat(headerInfos.get(0).getParameter("title"), Matchers.equalTo("Login to \\\"apps\\\"")); // TODO verify change from "Login to \"apps\""
|
||||
Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Basic"));
|
||||
Assert.assertTrue(headerInfos.get(1).getParameter("realm").equals("simple"));
|
||||
}
|
||||
|
@ -705,7 +704,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
Assert.assertTrue(headerInfo.getParameter("name").equals("value"));
|
||||
Assert.assertTrue(headerInfo.getParameter("other").equals("value2"));
|
||||
|
||||
headerInfos = aph.getHeaderInfo("Scheme name=value, Scheme2 name=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"));
|
||||
|
@ -768,4 +767,23 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest
|
|||
Assert.assertTrue(headerInfoList.get(2).getParameter("realm").equals("thermostat3="));
|
||||
Assert.assertTrue(headerInfoList.get(2).getParameter("nonce").equals("9523570528="));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleChallangeLooksLikeMultipleChallenge()
|
||||
{
|
||||
AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client);
|
||||
List<HeaderInfo> headerInfoList = aph.getHeaderInfo("Digest param=\",f \"");
|
||||
Assert.assertEquals(1, headerInfoList.size());
|
||||
|
||||
|
||||
|
||||
headerInfoList = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\",Digest realm=hello\", nonce=\"1523430383=\"");
|
||||
Assert.assertEquals(1, headerInfoList.size());
|
||||
|
||||
HeaderInfo headerInfo = headerInfoList.get(0);
|
||||
Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest"));
|
||||
Assert.assertTrue(headerInfo.getParameter("qop").equals(",Digest realm=hello"));
|
||||
Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat"));
|
||||
Assert.assertThat(headerInfo.getParameter("nonce"), Matchers.is("1523430383="));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue