Skip to content

Commit

Permalink
Merge pull request #83 from clescot/allow_proxy_and_site_authenticati…
Browse files Browse the repository at this point in the history
…on_in_the_same_time

- permit to cache Proxy authentication, and to combine with website authentication
  • Loading branch information
rburgst committed Aug 3, 2023
2 parents 33b7dc7 + 2657795 commit 0e7d304
Show file tree
Hide file tree
Showing 17 changed files with 674 additions and 149 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,25 @@ DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()

client = builder
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache,new DefaultRequestCacheKeyProvider()))
.addNetworkInterceptor(logger)
.build();
```
If you want to cache Proxy credentials, you need to add a NetworkInterceptor :

```java
client = builder
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addNetworkInterceptor(new AuthenticationCacheInterceptor(authCache,new DefaultProxyCacheKeyProvider()))
.addNetworkInterceptor(logger)
.build();
```
You can also combine Proxy AND Web site Authentication :
```java
client = builder
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addNetworkInterceptor(new AuthenticationCacheInterceptor(authCache,new DefaultProxyCacheKeyProvider()))
.addInterceptor(new AuthenticationCacheInterceptor(authCache,new DefaultRequestCacheKeyProvider()))
.addNetworkInterceptor(logger)
.build();
```
Expand Down
16 changes: 12 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ repositories {
}

ext {
okhttpVersion = "4.10.0"
mockitoVersion = "5.4.0"
okhttpVersion = "4.11.0"
}

compileJava.options.encoding = 'UTF-8'
Expand All @@ -35,9 +36,12 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
testImplementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.21.0'
testImplementation 'org.mockito:mockito-core:4.0.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:5.9.3"
testImplementation "org.junit.jupiter:junit-jupiter:5.9.3"
testImplementation "org.assertj:assertj-core:3.21.0"
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}"
testImplementation "com.github.tomakehurst:wiremock-jre8:2.35.0"
}

java {
Expand Down Expand Up @@ -111,3 +115,7 @@ tasks.withType(io.github.gradlenexus.publishplugin.InitializeNexusStagingReposit
shouldRunAfter(tasks.withType(Sign))
}

test {
useJUnitPlatform()
}

Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
package com.burgstaller.okhttp;

import com.burgstaller.okhttp.digest.CachingAuthenticator;
import okhttp3.*;
import okhttp3.internal.platform.Platform;

import java.io.IOException;
import java.util.Map;

import okhttp3.Connection;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.internal.platform.Platform;

import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;

/**
* An HTTP Request interceptor that adds previous auth headers in to the same host. This enables the
* client to reduce the number of 401 auth request/response cycles.
*/
public class AuthenticationCacheInterceptor implements Interceptor {
private final Map<String, CachingAuthenticator> authCache;
private final CacheKeyProvider cacheKeyProvider;
Expand All @@ -29,13 +20,18 @@ public AuthenticationCacheInterceptor(Map<String, CachingAuthenticator> authCach
}

public AuthenticationCacheInterceptor(Map<String, CachingAuthenticator> authCache) {
this(authCache, new DefaultCacheKeyProvider());
this(authCache, new DefaultRequestCacheKeyProvider());
}

@Override
public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
final String key = cacheKeyProvider.getCachingKey(request);
final String key;
if (cacheKeyProvider.applyToProxy()) {
key = cacheKeyProvider.getCachingKey(chain.connection().route().proxy());
} else {
key = cacheKeyProvider.getCachingKey(request);
}
CachingAuthenticator authenticator = authCache.get(key);
Request authRequest = null;
Connection connection = chain.connection();
Expand All @@ -50,7 +46,9 @@ public Response intercept(Chain chain) throws IOException {

// Cached response was used, but it produced unauthorized response (cache expired).
int responseCode = response != null ? response.code() : 0;
if (authenticator != null && (responseCode == HTTP_UNAUTHORIZED || responseCode == HTTP_PROXY_AUTH)) {

//authentication was against a web site
if (authenticator != null && (!cacheKeyProvider.applyToProxy() && responseCode == HTTP_UNAUTHORIZED)){
// Remove cached authenticator and resend request
if (authCache.remove(key) != null) {
response.body().close();
Expand All @@ -59,6 +57,14 @@ public Response intercept(Chain chain) throws IOException {
response = chain.proceed(request);
}
}
//authentication against a proxy
if (authenticator != null && (cacheKeyProvider.applyToProxy() && responseCode == HTTP_PROXY_AUTH)){
authCache.remove(key);
//interceptor at the proxy level is a Network Interceptor which does not permit to call proceed more than once.
//in this case, we don't close the request and call chain.proceed(request) another time
}

return response;
}
}

}
20 changes: 13 additions & 7 deletions src/main/java/com/burgstaller/okhttp/CacheKeyProvider.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package com.burgstaller.okhttp;

import okhttp3.Request;

/**
* Provides the caching key for the given request. Can be used to share passwords accross multiple subdomains.
* Provides the caching key for the given request or {@link java.net.Proxy}. Can be used to share passwords accross multiple subdomains.
* @see java.net.Proxy
*/
public interface CacheKeyProvider {
public interface CacheKeyProvider<T> {

/**
*
* @return true if the key is forged from a {@link java.net.Proxy} Object.
*/
boolean applyToProxy();
/**
* Provides the caching key for the given request. Can be used to share passwords accross multiple subdomains.
* Provides the caching key for the given request or {@link java.net.Proxy}. Can be used to share passwords accross multiple subdomains.
*
* @param request the http request.
* @param request the http request, or a {@link java.net.Proxy} if the applyToProxy method returns true.
* @return the cache key.
*/
String getCachingKey(Request request);
String getCachingKey(T request);
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,34 @@ public CachingAuthenticatorDecorator(Authenticator innerAuthenticator, Map<Strin
this.cacheKeyProvider = cacheKeyProvider;
}

public CachingAuthenticatorDecorator(Authenticator innerAuthenticator, Map<String, CachingAuthenticator> authCache) {
this(innerAuthenticator, authCache, new DefaultCacheKeyProvider());
public CachingAuthenticatorDecorator(Authenticator innerAuthenticator, Map<String, CachingAuthenticator> authCache,boolean proxy) {
this(innerAuthenticator, authCache, proxy?new DefaultProxyCacheKeyProvider():new DefaultRequestCacheKeyProvider());
}

public CachingAuthenticatorDecorator(Authenticator innerAuthenticator, Map<String, CachingAuthenticator> authCache) {
this(innerAuthenticator, authCache, false);
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
Request authenticated = innerAuthenticator.authenticate(route, response);
if (authenticated != null) {
String authorizationValue = authenticated.header("Authorization");
String authorizationValue;
if(cacheKeyProvider.applyToProxy()){
authorizationValue = authenticated.header("Proxy-Authorization");
}else{
authorizationValue = authenticated.header("Authorization");
}

if (authorizationValue != null && innerAuthenticator instanceof CachingAuthenticator) {
final String key = cacheKeyProvider.getCachingKey(authenticated);
String key;
if(cacheKeyProvider.applyToProxy()){
key = cacheKeyProvider.getCachingKey(route.proxy());
}else {
key = cacheKeyProvider.getCachingKey(authenticated);
}
authCache.put(key, (CachingAuthenticator) innerAuthenticator);
}
}
return authenticated;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.burgstaller.okhttp;

import java.net.Proxy;

/**
* The default version of the cache key provider, which simply calls the {@link java.net.Proxy#toString()} method to generate key.
*/
public final class DefaultProxyCacheKeyProvider implements CacheKeyProvider<Proxy> {
@Override
public boolean applyToProxy() {
return true;
}

/**
*
* @param proxy {@link java.net.Proxy} used to get the cache key.
* @return the cache key.
*/
@Override
public String getCachingKey(Proxy proxy) {
return proxy != null ? proxy.toString() : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
/**
* The default version of the cache key provider, which simply takes the request URL / port for
*/
public final class DefaultCacheKeyProvider implements CacheKeyProvider {
public final class DefaultRequestCacheKeyProvider implements CacheKeyProvider<Request> {
@Override
public boolean applyToProxy() {
return false;
}

/**
* Provides the caching key for the given request. Can be used to share passwords accross multiple subdomains.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.Credentials;

import org.junit.Before;
import org.junit.Test;
import okhttp3.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.net.SocketFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
Expand All @@ -18,32 +20,16 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import javax.net.SocketFactory;

import okhttp3.Address;
import okhttp3.Authenticator;
import okhttp3.Connection;
import okhttp3.ConnectionSpec;
import okhttp3.Dns;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.Route;

import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.BDDMockito.given;

/**
* Unit test for authenticator caching.
*
* @author Alexey Vasilyev
*/
@ExtendWith(MockitoExtension.class)
public class AuthenticationCacheInterceptorTest {

@Mock
Expand All @@ -60,9 +46,8 @@ public class AuthenticationCacheInterceptorTest {
@Mock
Proxy proxy;

@Before
@BeforeEach
public void beforeMethod() {
MockitoAnnotations.initMocks(this);

// setup some dummy data so that we dont get NPEs
Address address = new Address("localhost", 8080, mockDns, socketFactory, null, null,
Expand Down Expand Up @@ -110,7 +95,7 @@ public Response proceed(Request request) {

private void thenAuthCacheShouldBeEmpty(Map<String, CachingAuthenticator> authCache) {
// No cached authenticator anymore
assertEquals(0, authCache.size());
assertThat(authCache).isEmpty();
}

@Test
Expand All @@ -120,7 +105,7 @@ public void testCaching_withDifferentPorts() throws Exception {
// Fill in authCache.
// https://myhost.com => basic auth user1:user1
givenCachedAuthenticationFor("https://myhost.com", authCache);
assertEquals(1, authCache.size());
assertThat(authCache).hasSize(1);

Interceptor interceptor = new AuthenticationCacheInterceptor(authCache);

Expand All @@ -139,7 +124,7 @@ public void testCaching__whenNoConnectionExists__shouldNotBombOut() throws IOExc
Interceptor interceptor = new AuthenticationCacheInterceptor(authCache);

String auth = whenInterceptAuthenticationForUrlWithNoConnection(interceptor, "https://myhost.com:443");
assertNull(auth);
assertThat(auth).isNull();
}

@Test
Expand All @@ -154,7 +139,7 @@ public void testCaching__whenNoConnectionExistsButCachedInfo__shouldNotBombOut()
}

private void thenNoAuthorizationHeaderShouldBePresent(String authorization2) {
assertNull(authorization2);
assertThat(authorization2).isNull();
}

private void thenAuthorizationHeaderShouldBePresent(String authorization) {
Expand Down

0 comments on commit 0e7d304

Please sign in to comment.