Commit ff40bfd6 authored by Ricki Hirner's avatar Ricki Hirner

Authentication improvements

* use preemptive Basic authentication automatically for HTTPS connections
* cache successful authentication (Basic/Digest)
parent bf2004e9
......@@ -14,11 +14,13 @@ import okhttp3.RequestBody;
import junit.framework.TestCase;
public class BasicDigestAuthenticatorTest extends TestCase {
public class BasicDigestAuthHandlerTest extends TestCase {
public void testRFCExample() {
// use cnonce from example
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "Mufasa", "Circle Of Life", "0a4f113b");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "Mufasa", "Circle Of Life");
authenticator.clientNonce = "0a4f113b";
authenticator.nonceCount.set(1);
// construct WWW-Authenticate
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
......@@ -31,7 +33,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.get()
.url("http://www.nowhere.org/dir/index.html")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
Request request = authenticator.digestRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"Mufasa\""));
assertTrue(auth.contains("realm=\"testrealm@host.com\""));
......@@ -45,7 +47,9 @@ public class BasicDigestAuthenticatorTest extends TestCase {
}
public void testRealWorldExamples() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "demo", "demo", "MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "demo", "demo");
authenticator.clientNonce = "MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=";
authenticator.nonceCount.set(1);
// example 1
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
......@@ -58,7 +62,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.method("PROPFIND", null)
.url("https://demo.group-office.eu/caldav/")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
Request request = authenticator.digestRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"demo\""));
assertTrue(auth.contains("realm=\"Group-Office\""));
......@@ -71,7 +75,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
assertTrue(auth.contains("opaque=\"df58bdff8cf60599c939187d0b5c54de\""));
// example 2
authenticator = new BasicDigestAuthenticator(null, "test", "test");
authenticator = new BasicDigestAuthHandler(null, "test", "test");
authScheme = new HttpUtils.AuthScheme("digest"); // lower case
authScheme.params.put("nonce", "87c4c2aceed9abf30dd68c71");
authScheme.params.put("algorithm", "md5"); // note the (illegal) lower case!
......@@ -81,7 +85,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.method("OPTIONS", null)
.url("https://ieddy.ru/")
.build();
request = authenticator.authorizationRequest(original, authScheme);
request = authenticator.digestRequest(original, authScheme);
auth = request.header("Authorization");
assertTrue(auth.contains("algorithm=\"MD5\"")); // some servers require it
assertTrue(auth.contains("username=\"test\""));
......@@ -89,14 +93,16 @@ public class BasicDigestAuthenticatorTest extends TestCase {
assertTrue(auth.contains("nonce=\"87c4c2aceed9abf30dd68c71\""));
assertTrue(auth.contains("uri=\"/\""));
assertFalse(auth.contains("cnonce="));
assertFalse(auth.contains("nc="));
assertFalse(auth.contains("nc=00000001"));
assertFalse(auth.contains("qop="));
assertTrue(auth.contains("response=\"d42a39f25f80b0d6907286a960ff9c7d\""));
assertTrue(auth.contains("opaque=\"571609eb7058505d35c7bf7288fbbec4-ODdjNGMyYWNlZWQ5YWJmMzBkZDY4YzcxLDAuMC4wLjAsMTQ0NTM3NzE0Nw==\""));
}
public void testMD5Sess() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "admin", "12345", "hxk1lu63b6c7vhk");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "admin", "12345");
authenticator.clientNonce = "hxk1lu63b6c7vhk";
authenticator.nonceCount.set(1);
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "MD5-sess Example");
......@@ -117,7 +123,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.method("POST", RequestBody.create(MediaType.parse("text/plain"), "PLAIN TEXT"))
.url("http://example.com/plain.txt")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
Request request = authenticator.digestRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"admin\""));
assertTrue(auth.contains("realm=\"MD5-sess Example\""));
......@@ -131,7 +137,9 @@ public class BasicDigestAuthenticatorTest extends TestCase {
}
public void testMD5AuthInt() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "admin", "12435", "hxk1lu63b6c7vhk");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "admin", "12435");
authenticator.clientNonce = "hxk1lu63b6c7vhk";
authenticator.nonceCount.set(1);
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
authScheme.params.put("realm", "AuthInt Example");
......@@ -152,7 +160,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.method("POST", RequestBody.create(MediaType.parse("text/plain"), "PLAIN TEXT"))
.url("http://example.com/plain.txt")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
Request request = authenticator.digestRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"admin\""));
assertTrue(auth.contains("realm=\"AuthInt Example\""));
......@@ -166,7 +174,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
}
public void testLegacyDigest() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "Mufasa", "CircleOfLife");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "Mufasa", "CircleOfLife");
// construct WWW-Authenticate
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
......@@ -178,7 +186,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.get()
.url("http://www.nowhere.org/dir/index.html")
.build();
Request request = authenticator.authorizationRequest(original, authScheme);
Request request = authenticator.digestRequest(original, authScheme);
String auth = request.header("Authorization");
assertTrue(auth.contains("username=\"Mufasa\""));
assertTrue(auth.contains("realm=\"testrealm@host.com\""));
......@@ -192,7 +200,7 @@ public class BasicDigestAuthenticatorTest extends TestCase {
}
public void testIncompleteAuthenticationRequests() {
BasicDigestAuthenticator authenticator = new BasicDigestAuthenticator(null, "demo", "demo");
BasicDigestAuthHandler authenticator = new BasicDigestAuthHandler(null, "demo", "demo");
Request original = new Request.Builder()
.get()
......@@ -200,16 +208,16 @@ public class BasicDigestAuthenticatorTest extends TestCase {
.build();
HttpUtils.AuthScheme authScheme = new HttpUtils.AuthScheme("Digest");
assertNull(authenticator.authorizationRequest(original, authScheme));
assertNull(authenticator.digestRequest(original, authScheme));
authScheme.params.put("realm", "Group-Office");
assertNull(authenticator.authorizationRequest(original, authScheme));
assertNull(authenticator.digestRequest(original, authScheme));
authScheme.params.put("qop", "auth");
assertNull(authenticator.authorizationRequest(original, authScheme));
assertNull(authenticator.digestRequest(original, authScheme));
authScheme.params.put("nonce", "56212407212c8");
assertNotNull(authenticator.authorizationRequest(original, authScheme));
assertNotNull(authenticator.digestRequest(original, authScheme));
}
}
......@@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import lombok.NonNull;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
......@@ -26,81 +27,96 @@ import okhttp3.Route;
import okio.Buffer;
import okio.ByteString;
public class BasicDigestAuthenticator implements Authenticator {
/**
* Handler to manage authentication against a given service (may be limited to one host).
* There's no domain-based cache, because the same user name and password will be used for
* all requests.
*
* Authentication methods/credentials found to be working will be cached for further requests
* (this is why the interceptor is needed).
*
* Usage: Set as authenticator <b>and</b> as network interceptor.
*/
public class BasicDigestAuthHandler implements Authenticator, Interceptor {
protected static final String
HEADER_AUTHENTICATE = "WWW-Authenticate",
HEADER_AUTHORIZATION = "Authorization";
final String host, username, password;
final String clientNonce;
AtomicInteger nonceCount = new AtomicInteger(1);
// cached authentication schemes
HttpUtils.AuthScheme basicAuth, digestAuth;
public BasicDigestAuthenticator(String host, String username, String password) {
this.host = host;
this.username = username;
this.password = password;
// cached digest parameters
static String clientNonce = h(UUID.randomUUID().toString());
static final AtomicInteger nonceCount = new AtomicInteger(1);
clientNonce = h(UUID.randomUUID().toString());
}
BasicDigestAuthenticator(String host, String username, String password, String clientNonce) {
this.host = null;
public BasicDigestAuthHandler(String host, String username, String password) {
this.host = host;
this.username = username;
this.password = password;
this.clientNonce = clientNonce;
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
Request request = response.request();
protected Request authenticateRequest(Request request, Response response) {
if (host != null && !request.url().host().equalsIgnoreCase(host)) {
Constants.log.warning("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;
if (response == null) {
// we're not processing a 401 response
HttpUtils.AuthScheme basicAuth = null, digestAuth = null;
for (HttpUtils.AuthScheme scheme : HttpUtils.parseWwwAuthenticate(response.headers(HEADER_AUTHENTICATE).toArray(new String[0])))
if ("Basic".equalsIgnoreCase(scheme.name))
basicAuth = scheme;
else if ("Digest".equalsIgnoreCase(scheme.name))
digestAuth = scheme;
if (basicAuth == null && digestAuth == null && request.isHttps()) {
Constants.log.fine("Trying Basic auth preemptively");
basicAuth = new HttpUtils.AuthScheme("Basic");
}
// we MUST prefer Digest auth [https://tools.ietf.org/html/rfc2617#section-4.6]
if (digestAuth != null) {
// Digest auth
} else {
// we're processing a 401 response
HttpUtils.AuthScheme newBasicAuth = null, newDigestAuth = null;
for (HttpUtils.AuthScheme scheme : HttpUtils.parseWwwAuthenticate(response.headers(HEADER_AUTHENTICATE).toArray(new String[0])))
if ("Basic".equalsIgnoreCase(scheme.name)) {
if (basicAuth != null) {
Constants.log.warning("Basic credentials didn't work last time -> aborting");
basicAuth = null;
return null;
}
newBasicAuth = scheme;
} else if ("Digest".equalsIgnoreCase(scheme.name)) {
if (digestAuth != null && !"true".equalsIgnoreCase(scheme.params.get("stale"))) {
Constants.log.warning("Digest credentials didn't work last time and server nonce has not expired -> aborting");
digestAuth = null;
return null;
}
newDigestAuth = scheme;
}
if (triedBefore && !"true".equalsIgnoreCase(digestAuth.params.get("stale")))
// credentials didn't work last time, and they won't work now -> stop here
return null;
basicAuth = newBasicAuth;
digestAuth = newDigestAuth;
}
// we MUST prefer Digest auth [https://tools.ietf.org/html/rfc2617#section-4.6]
if (digestAuth != null) {
Constants.log.fine("Adding Digest authorization request for " + request.url());
return authorizationRequest(request, digestAuth);
return digestRequest(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;
Constants.log.fine("Adding Basic authorization header for " + request.url());
return request.newBuilder()
.header(HEADER_AUTHORIZATION, Credentials.basic(username, password))
.build();
} else
Constants.log.severe("No supported authentication scheme");
} else if (response != null)
Constants.log.warning("No supported authentication scheme");
// no supported auth scheme
return null;
}
protected Request authorizationRequest(Request request, HttpUtils.AuthScheme digest) {
protected Request digestRequest(Request request, HttpUtils.AuthScheme digest) {
String realm = digest.params.get("realm"),
opaque = digest.params.get("opaque"),
nonce = digest.params.get("nonce");
......@@ -181,21 +197,21 @@ public class BasicDigestAuthenticator implements Authenticator {
return null;
}
protected String quotedString(String s) {
protected static String quotedString(String s) {
return "\"" + s.replace("\"", "\\\"") + "\"";
}
protected String h(String data) {
protected static String h(String data) {
return ByteString.of(data.getBytes()).md5().hex();
}
protected String h(@NonNull RequestBody body) throws IOException {
protected static 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) {
protected static String kd(String secret, String data) {
return h(secret + ":" + data);
}
......@@ -245,4 +261,22 @@ public class BasicDigestAuthenticator implements Authenticator {
}
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
return authenticateRequest(response.request(), response);
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (request.header(HEADER_AUTHORIZATION) == null) {
// try to apply cached authentication
Request authRequest = authenticateRequest(request, null);
if (authRequest != null)
request = authRequest;
}
return chain.proceed(request);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment