Support Basic and Digest auth

parent b1ca2ff7
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import junit.framework.TestCase;
public class BasicDigestAuthenticatorTest extends TestCase {
public void testRFCExample() {
// use cnonce from example
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "Mufasa", "Circle Of Life", "0a4f113b");
// construct WWW-Authenticate
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "testrealm@host.com");
authScheme.params.put("qop", "auth");
authScheme.params.put("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093");
authScheme.params.put("opaque", "5ccc069c403ebaf9f0171e9517f40e41");
Request original = new Request.Builder()
.get()
.url("http://www.nowhere.org/dir/index.html")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"Mufasa\""));
assertTrue(auth.contains("realm=\"testrealm@host.com\""));
assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""));
assertTrue(auth.contains("uri=\"/dir/index.html\""));
assertTrue(auth.contains("qop=auth"));
assertTrue(auth.contains("nc=00000001"));
assertTrue(auth.contains("cnonce=\"0a4f113b\""));
assertTrue(auth.contains("response=\"6629fae49393a05397450978507c4ef1\""));
assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""));
}
public void testRealWorldExample() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "demo", "demo", "MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=");
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "Group-Office");
authScheme.params.put("qop", "auth");
authScheme.params.put("nonce", "56212407212c8");
authScheme.params.put("opaque", "df58bdff8cf60599c939187d0b5c54de");
Request original = new Request.Builder()
.method("PROPFIND", null)
.url("https://demo.group-office.eu/caldav/")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"demo\""));
assertTrue(auth.contains("realm=\"Group-Office\""));
assertTrue(auth.contains("nonce=\"56212407212c8\""));
assertTrue(auth.contains("uri=\"/caldav/\""));
assertTrue(auth.contains("cnonce=\"MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=\""));
assertTrue(auth.contains("nc=00000001"));
assertTrue(auth.contains("qop=auth"));
assertTrue(auth.contains("response=\"de3b3b194d85ddc62537208c9c3637dc\""));
assertTrue(auth.contains("opaque=\"df58bdff8cf60599c939187d0b5c54de\""));
}
public void testMD5Sess() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "admin", "12345", "hxk1lu63b6c7vhk");
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "MD5-sess Example");
authScheme.params.put("qop", "auth");
authScheme.params.put("algorithm", "MD5-sess");
authScheme.params.put("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093");
authScheme.params.put("opaque", "5ccc069c403ebaf9f0171e9517f40e41");
/* A1 = h("admin:MD5-sess Example:12345"):dcd98b7102dd2f0e8b11d0f600bfb0c093:hxk1lu63b6c7vhk =
4eaed818bc587129e73b39c8d3e8425a:dcd98b7102dd2f0e8b11d0f600bfb0c093:hxk1lu63b6c7vhk a994ee9d33e2f077d3a6e13e882f6686
A2 = POST:/plain.txt 1b557703454e1aa1230c5523f54380ed
h("a994ee9d33e2f077d3a6e13e882f6686:dcd98b7102dd2f0e8b11d0f600bfb0c093:00000001:hxk1lu63b6c7vhk:auth:1b557703454e1aa1230c5523f54380ed") =
af2a72145775cfd08c36ad2676e89446
*/
Request original = new Request.Builder()
.method("POST", RequestBody.create(MediaType.parse("text/plain"), "PLAIN TEXT"))
.url("http://example.com/plain.txt")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"admin\""));
assertTrue(auth.contains("realm=\"MD5-sess Example\""));
assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""));
assertTrue(auth.contains("uri=\"/plain.txt\""));
assertTrue(auth.contains("cnonce=\"hxk1lu63b6c7vhk\""));
assertTrue(auth.contains("nc=00000001"));
assertTrue(auth.contains("qop=auth"));
assertTrue(auth.contains("response=\"af2a72145775cfd08c36ad2676e89446\""));
assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""));
}
public void testMD5AuthInt() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "admin", "12435", "hxk1lu63b6c7vhk");
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "AuthInt Example");
authScheme.params.put("qop", "auth-int");
authScheme.params.put("nonce", "367sj3265s5");
authScheme.params.put("opaque", "87aaxcval4gba36");
/* A1 = admin:AuthInt Example:12345 380dc3fc1305127cd2aa81ab68ef3f34
h("PLAIN TEXT") = 20296edbd4c4275fb416b64e4be752f9
A2 = POST:/plain.txt:20296edbd4c4275fb416b64e4be752f9 a71c4c86e18b3993ffc98c6e426fe4b0
h(380dc3fc1305127cd2aa81ab68ef3f34:367sj3265s5:00000001:hxk1lu63b6c7vhk:auth-int:a71c4c86e18b3993ffc98c6e426fe4b0) =
81d07cb3b8d412b34144164124c970cb
*/
Request original = new Request.Builder()
.method("POST", RequestBody.create(MediaType.parse("text/plain"), "PLAIN TEXT"))
.url("http://example.com/plain.txt")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"admin\""));
assertTrue(auth.contains("realm=\"AuthInt Example\""));
assertTrue(auth.contains("nonce=\"367sj3265s5\""));
assertTrue(auth.contains("uri=\"/plain.txt\""));
assertTrue(auth.contains("cnonce=\"hxk1lu63b6c7vhk\""));
assertTrue(auth.contains("nc=00000001"));
assertTrue(auth.contains("qop=auth-int"));
assertTrue(auth.contains("response=\"5ab6822b9d906cc711760a7783b28dca\""));
assertTrue(auth.contains("opaque=\"87aaxcval4gba36\""));
}
public void testLegacyDigest() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "Mufasa", "CircleOfLife");
// construct WWW-Authenticate
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "testrealm@host.com");
authScheme.params.put("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093");
authScheme.params.put("opaque", "5ccc069c403ebaf9f0171e9517f40e41");
Request original = new Request.Builder()
.get()
.url("http://www.nowhere.org/dir/index.html")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"Mufasa\""));
assertTrue(auth.contains("realm=\"testrealm@host.com\""));
assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""));
assertTrue(auth.contains("uri=\"/dir/index.html\""));
assertFalse(auth.contains("qop="));
assertFalse(auth.contains("nc="));
assertFalse(auth.contains("cnonce="));
assertTrue(auth.contains("response=\"1949323746fe6a43ef61f9606e7febea\""));
assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""));
}
public void testIncompleteAuthenticationRequests() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "demo", "demo");
Request original = new Request.Builder()
.get()
.url("http://www.nowhere.org/dir/index.html")
.build();
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
assertNull(authenticator.authorizationRequest(original, authScheme));
authScheme.params.put("realm", "Group-Office");
assertNull(authenticator.authorizationRequest(original, authScheme));
authScheme.params.put("qop", "auth");
assertNull(authenticator.authorizationRequest(original, authScheme));
authScheme.params.put("nonce", "56212407212c8");
assertNull(authenticator.authorizationRequest(original, authScheme));
authScheme.params.put("opaque", "df58bdff8cf60599c939187d0b5c54de");
assertNotNull(authenticator.authorizationRequest(original, authScheme));
}
}
......@@ -8,10 +8,6 @@
package at.bitfire.dav4android;
import android.util.Log;
import com.squareup.okhttp.HttpUrl;
import junit.framework.TestCase;
import java.util.List;
......@@ -19,24 +15,116 @@ import java.util.List;
public class HttpUtilsTest extends TestCase {
public void testParseWwwAuthenticate() {
List<HttpUtils.AuthScheme> schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "Basic realm=\"test\"" });
// two schemes: one without param (illegal!), second with two params
List<HttpUtils.AuthScheme> schemes = HttpUtils.parseWwwAuthenticate(new String[]{ " UnknownWithoutParam, Unknown WithParam1=\"a\", Param2 " });
assertEquals(2, schemes.size());
assertEquals("UnknownWithoutParam", schemes.get(0).name);
assertEquals(0, schemes.get(0).params.size());
assertEquals(0, schemes.get(0).unnamedParams.size());
assertEquals("Unknown", schemes.get(1).name);
assertEquals(1, schemes.get(1).params.size());
assertEquals("a", schemes.get(1).params.get("WithParam1"));
assertEquals(1, schemes.get(1).params.size());
assertEquals(1, schemes.get(1).unnamedParams.size());
assertEquals("Param2", schemes.get(1).unnamedParams.get(0));
// parameters with quoted strings with commas
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "X-MyScheme param1, param2=\"a,\\\"b\\\",c\", MyOtherScheme paramA" });
assertEquals(2, schemes.size());
assertEquals("X-MyScheme", schemes.get(0).name);
assertEquals(1, schemes.get(0).params.size());
assertEquals("a,\"b\",c", schemes.get(0).params.get("param2"));
assertEquals(1, schemes.get(0).unnamedParams.size());
assertEquals("param1", schemes.get(0).unnamedParams.get(0));
assertEquals("MyOtherScheme", schemes.get(1).name);
assertEquals(0, schemes.get(1).params.size());
assertEquals(1, schemes.get(1).unnamedParams.size());
assertEquals("paramA", schemes.get(1).unnamedParams.get(0));
/*** REAL WORLD EXAMPLES ***/
// Basic auth
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "Basic realm=\"test\"" });
assertEquals(1, schemes.size());
HttpUtils.AuthScheme scheme = schemes.get(0);
assertEquals("Basic", scheme.scheme);
assertEquals("Basic", scheme.name);
assertEquals(1, scheme.params.size());
assertEquals("realm=\"test\"", scheme.params.get(0));
assertEquals("test", scheme.params.get("realm"));
assertEquals(0, scheme.unnamedParams.size());
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ " UnknownWithoutParam, Unknown WithParam1, Param2 " });
// Basic and Digest auth in one line
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "Basic realm=\"testrealm@host.com\", Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"" });
assertEquals(2, schemes.size());
assertEquals("UnknownWithoutParam", schemes.get(0).scheme);
assertEquals(0, schemes.get(0).params.size());
assertEquals("Unknown", schemes.get(1).scheme);
assertEquals(2, schemes.get(1).params.size());
assertEquals("WithParam1", schemes.get(1).params.get(0));
assertEquals("Param2", schemes.get(1).params.get(1));
scheme = schemes.get(0);
assertEquals("Basic", scheme.name);
assertEquals(1, scheme.params.size());
assertEquals("testrealm@host.com", scheme.params.get("realm"));
assertEquals(0, scheme.unnamedParams.size());
// TODO test parameters with quoted strings with commas:
// X-MyScheme param1, param2="a,b,c", MyOtherScheme paramA
scheme = schemes.get(1);
assertEquals("Digest", scheme.name);
assertEquals(4, scheme.params.size());
assertEquals("testrealm@host.com", scheme.params.get("realm"));
assertEquals("auth,auth-int", scheme.params.get("qop"));
assertEquals("dcd98b7102dd2f0e8b11d0f600bfb0c093", scheme.params.get("nonce"));
assertEquals("5ccc069c403ebaf9f0171e9517f40e41", scheme.params.get("opaque"));
assertEquals(0, scheme.unnamedParams.size());
// Negotiate (RFC 4559)
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "Negotiate" });
assertEquals(1, schemes.size());
scheme = schemes.get(0);
assertEquals("Negotiate", scheme.name);
assertEquals(0, scheme.params.size());
assertEquals(0, scheme.unnamedParams.size());
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "Negotiate a87421000492aa874209af8bc028" });
assertEquals(1, schemes.size());
scheme = schemes.get(0);
assertEquals("Negotiate", scheme.name);
assertEquals(0, scheme.params.size());
assertEquals(1, scheme.unnamedParams.size());
assertEquals("a87421000492aa874209af8bc028", scheme.unnamedParams.get(0));
// NTLM, see https://msdn.microsoft.com/en-us/library/dd944123%28v=office.12%29.aspx
schemes = HttpUtils.parseWwwAuthenticate(new String[]{
"NTLM realm=\"SIP Communications Service\", targetname=\"server.contoso.com\", version=3",
"Kerberos realm=\"SIP Communications Service\", targetname=\"sip/server.contoso.com\", version=3"
});
assertEquals(2, schemes.size());
scheme = schemes.get(0);
assertEquals("NTLM", scheme.name);
assertEquals(3, scheme.params.size());
assertEquals("SIP Communications Service", scheme.params.get("realm"));
assertEquals("server.contoso.com", scheme.params.get("targetname"));
assertEquals("3", scheme.params.get("version"));
assertEquals(0, scheme.unnamedParams.size());
scheme = schemes.get(1);
assertEquals("Kerberos", scheme.name);
assertEquals(3, scheme.params.size());
assertEquals("SIP Communications Service", scheme.params.get("realm"));
assertEquals("sip/server.contoso.com", scheme.params.get("targetname"));
assertEquals("3", scheme.params.get("version"));
assertEquals(0, scheme.unnamedParams.size());
// https://issues.apache.org/jira/browse/HTTPCLIENT-1489
schemes = HttpUtils.parseWwwAuthenticate(new String[]{ "X-MobileMe-AuthToken realm=\"Newcastle\", Basic realm=\"Newcastle\"" });
assertEquals(2, schemes.size());
scheme = schemes.get(0);
assertEquals("X-MobileMe-AuthToken", scheme.name);
assertEquals(1, scheme.params.size());
assertEquals("Newcastle", scheme.params.get("realm"));
assertEquals(0, scheme.unnamedParams.size());
scheme = schemes.get(1);
assertEquals("Basic", scheme.name);
assertEquals(1, scheme.params.size());
assertEquals("Newcastle", scheme.params.get("realm"));
assertEquals(0, scheme.unnamedParams.size());
}
}
/*
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.dav4android;
import android.text.TextUtils;
import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import java.io.IOException;
import java.net.Proxy;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.NonNull;
import okio.Buffer;
import okio.ByteString;
public class BasicDigestAuthenticator implements Authenticator {
protected static final String
HEADER_AUTHENTICATE = "WWW-Authenticate",
HEADER_AUTHORIZATION = "Authorization";
final String host, username, password;
final String clientNonce;
AtomicInteger nonceCount = new AtomicInteger(1);
public BasicDigestAuthenticator(String host, String username, String password) {
this.host = host;
this.username = username;
this.password = password;
clientNonce = h(UUID.randomUUID().toString());
}
BasicDigestAuthenticator(String host, String username, String password, String clientNonce) {
this.host = null;
this.username = username;
this.password = password;
this.clientNonce = clientNonce;
}
@Override
public Request authenticate(Proxy proxy, Response response) throws IOException {
Request request = response.request();
if (host != null && !request.httpUrl().host().equalsIgnoreCase(host)) {
Constants.log.warn("Not authenticating against " + host + " for security reasons!");
return null;
}
// check whether this is the first authentication try with our credentials
Response priorResponse = response.priorResponse();
boolean triedBefore = priorResponse != null ? priorResponse.request().header(HEADER_AUTHORIZATION) != null : false;
HttpUtils.AuthScheme basicAuth = null, digestAuth = null;
for (HttpUtils.AuthScheme scheme : HttpUtils.parseWwwAuthenticate(response.headers(HEADER_AUTHENTICATE).toArray(new String[0])))
if ("Basic".equals(scheme.name))
basicAuth = scheme;
else if ("Digest".equals(scheme.name))
digestAuth = scheme;
// we MUST prefer Digest auth [https://tools.ietf.org/html/rfc2617#section-4.6]
if (digestAuth != null) {
// Digest auth
if (triedBefore && !"true".equalsIgnoreCase(digestAuth.params.get("stale")))
// credentials didn't work last time, and they won't work now -> stop here
return null;
return authorizationRequest(request, digestAuth);
} else if (basicAuth != null) {
// Basic auth
if (triedBefore) // credentials didn't work last time, and they won't work now -> stop here
return null;
return request.newBuilder()
.header(HEADER_AUTHORIZATION, Credentials.basic(username, password))
.build();
}
// no supported auth scheme
return null;
}
@Override
public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
return null;
}
protected Request authorizationRequest(Request request, HttpUtils.AuthScheme digest) {
String realm = digest.params.get("realm"),
opaque = digest.params.get("opaque"),
nonce = digest.params.get("nonce");
Algorithm algorithm = Algorithm.determine(digest.params.get("algorithm"));
Protection qop = Protection.selectFrom(digest.params.get("qop"));
// build response parameters
String response = null;
List<String> params = new LinkedList<>();
params.add("username=" + quotedString(username));
if (realm != null)
params.add("realm=" + quotedString(realm));
else
return null;
if (nonce != null)
params.add("nonce=" + quotedString(nonce));
else
return null;
if (opaque != null)
params.add("opaque=" + quotedString(opaque));
else
return null;
final String method = request.method();
final String digestURI = request.httpUrl().encodedPath();
params.add("uri=" + quotedString(digestURI));
if (qop != null) {
params.add("qop=" + qop.name);
params.add("cnonce=" + quotedString(clientNonce));
int nc = nonceCount.getAndIncrement();
String ncValue = String.format("%08x", nc);
params.add("nc=" + ncValue);
String a1 = null;
if (algorithm == Algorithm.MD5)
a1 = username + ":" + realm + ":" + password;
else if (algorithm == Algorithm.MD5_SESSION)
a1 = h(username + ":" + realm + ":" + password) + ":" + nonce + ":" + clientNonce;
//Constants.log.trace("A1=" + a1);
String a2 = null;
if (qop == Protection.Auth)
a2 = method + ":" + digestURI;
else if (qop == Protection.AuthInt)
try {
RequestBody body = request.body();
a2 = method + ":" + digestURI + ":" + (body != null ? h(body) : h(""));
} catch(IOException e) {
Constants.log.warn("Couldn't get entity-body for hash calculation");
}
//Constants.log.trace("A2=" + a2);
if (a1 != null && a2 != null)
response = kd(h(a1), nonce + ":" + ncValue + ":" + clientNonce + ":" + qop.name + ":" + h(a2));
} else {
// legacy (backwards compatibility with RFC 2069)
if (algorithm == Algorithm.MD5) {
String a1 = username + ":" + realm + ":" + password,
a2 = method + ":" + digestURI;
response = kd(h(a1), nonce + ":" + h(a2));
}
}
if (response != null) {
params.add("response=" + quotedString(response));
return request.newBuilder()
.header(HEADER_AUTHORIZATION, "Digest " + TextUtils.join(", ", params))
.build();
} else
return null;
}
protected String quotedString(String s) {
return "\"" + s.replace("\"", "\\\"") + "\"";
}
protected String h(String data) {
return ByteString.of(data.getBytes()).md5().hex();
}
protected String h(@NonNull RequestBody body) throws IOException {
Buffer buffer = new Buffer();
body.writeTo(buffer);
return ByteString.of(buffer.readByteArray()).md5().hex();
}
protected String kd(String secret, String data) {
return h(secret + ":" + data);
}
protected enum Algorithm {
MD5("MD5"),
MD5_SESSION("MD5-sess");
public final String name;
Algorithm(String name) { this.name = name; }
static Algorithm determine(String paramValue) {
if (paramValue == null || Algorithm.MD5.name.equals(paramValue))
return Algorithm.MD5;
else if (Algorithm.MD5_SESSION.name.equals(paramValue))
return Algorithm.MD5_SESSION;
else
Constants.log.warn("Ignoring unknown hash algorithm: " + paramValue);
return null;
}
}
protected enum Protection { // quality of protection:
Auth("auth"), // authentication only
AuthInt("auth-int"); // authentication with integrity protection
public final String name;
Protection(String name) { this.name = name; }
static Protection selectFrom(String paramValue) {
if (paramValue != null) {
boolean qopAuth = false,
qopAuthInt = false;
for (String qop : paramValue.split(","))
if ("auth".equals(qop))
qopAuth = true;
else if ("auth-int".equals(qop))
qopAuthInt = true;
// prefer auth
if (qopAuth)
return Auth;
else if (qopAuthInt)
return AuthInt;
}
return null;
}
}
}
\ No newline at end of file
......@@ -8,52 +8,138 @@
package at.bitfire.dav4android;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
public class HttpUtils {
private static final Pattern authSchemeWithParam = Pattern.compile("^([^ ]+) +(.*)$");
public static List<AuthScheme> parseWwwAuthenticate(String[] wwwAuths) {
/* WWW-Authenticate = "WWW-Authenticate" ":" 1#challenge
challenge = auth-scheme 1*SP 1#auth-param
auth-scheme = token
auth-param = token "=" ( token | quoted-string )
We call the auth-param tokens: <name>=<value>
token = 1*<any CHAR except CTLs or separators>
separators = "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\" | <">
| "/" | "[" | "]" | "?" | "="
| "{" | "}" | SP | HT
quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
qdtext = <any TEXT except <">>
quoted-pair = "\" CHAR
*/
List<AuthScheme> schemes = new LinkedList<>();
for (String wwwAuth : wwwAuths) {
StringTokenizer tok = new StringTokenizer(wwwAuth.trim(), ",");
// Step 1: tokenize by ',', but take into account that auth-param values may contain quoted-pair values with ',' in it (these ',' have to be ignored)
// Auth-scheme and auth-param names are tokens and thus must not contain the '"' separator.
List<String> tokens = new LinkedList<>();
StringBuilder token = new StringBuilder();
boolean inQuotes = false;
int len = wwwAuth.length();
for (int i = 0; i < len; i++) {
char c = wwwAuth.charAt(i);
boolean literal = false;
if (c == '"')
inQuotes = !inQuotes;
else if (inQuotes && c == '\\' && i+1 < len) {
token.append(c);
c = wwwAuth.charAt(++i);
literal = true;
}
if (c == ',' && !inQuotes && !literal) {
tokens.add(token.toString());
token = new StringBuilder();
} else
token.append(c);
}
if (token.length() != 0)
tokens.add(token.toString());
/* Step 2: determine token type after trimming:
"<authSchemes> <auth-param>" new auth scheme + 1 param
"<auth-param>" add param to previous auth scheme
Take into account that the second type may contain quoted spaces.
The auth scheme name must not contain separators (including quotes).
*/
List<AuthScheme> authSchemes = new LinkedList<>();
List<String> authParams = new LinkedList<>();
AuthScheme scheme = null;
while (tok.hasMoreTokens()) {
String token = tok.nextToken().trim();
Constants.log.debug("Token: " + token);
if (token.contains(" ")) {
String parts[] = token.split(" +");