Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: primitive support for multiple WWW-Authenticate response headers (GoogleContainerTools#4187) #4188

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,30 @@ public ImagesAndRegistryClient call()
.setCredential(credential)
.newRegistryClient();

String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate();
if (wwwAuthenticate != null) {
eventHandlers.dispatch(
LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate));
registryClient.authPullByWwwAuthenticate(wwwAuthenticate);
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressDispatcher.newChildProducer()),
registryClient);
List<String> wwwAuthenticateList =
ex.getHttpResponseException().getHeaders().getAuthenticateAsList();
if (wwwAuthenticateList != null && !wwwAuthenticateList.isEmpty()) {
RegistryException storedEx = null;
// try all WWW-Authenticate headers until one succeeds
for (String wwwAuthenticate : wwwAuthenticateList) {
eventHandlers.dispatch(
LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate));
try {
storedEx = null;
registryClient.authPullByWwwAuthenticate(wwwAuthenticate);
break;
} catch (RegistryException exc) {
eventHandlers.dispatch(
LogEvent.debug(
"WWW-Authenticate failed for " + imageReference + ": " + wwwAuthenticate));
storedEx = exc;
}
}
// if none of the WWW-Authenticate headers worked for the authPullByWwwAuthenticate, throw
// the last stored exception
if (storedEx != null) {
throw storedEx;
}

} else {
// Not getting WWW-Authenticate is unexpected in practice, and we may just blame the
Expand All @@ -200,10 +216,9 @@ public ImagesAndRegistryClient call()
eventHandlers.dispatch(
LogEvent.debug("Trying bearer auth as fallback for " + imageReference + "..."));
registryClient.doPullBearerAuth();
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressDispatcher.newChildProducer()),
registryClient);
}
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressDispatcher.newChildProducer()), registryClient);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,31 @@ public Optional<RegistryAuthenticator> handleHttpResponseException(
}

// Checks if the 'WWW-Authenticate' header is present.
String authenticationMethod = responseException.getHeaders().getAuthenticate();
if (authenticationMethod == null) {
List<String> authList = responseException.getHeaders().getAuthenticateAsList();
if (authList == null || authList.isEmpty()) {
throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException)
.addReason("'WWW-Authenticate' header not found")
.build();
}

// Parses the header to retrieve the components.
try {
return RegistryAuthenticator.fromAuthenticationMethod(
authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient);

} catch (RegistryAuthenticationFailedException ex) {
throw new RegistryErrorExceptionBuilder(getActionDescription(), ex)
.addReason("Failed get authentication method from 'WWW-Authenticate' header")
.build();
// try all 'WWW-Authenticate' headers until a working RegistryAuthenticator can be created
RegistryErrorException lastExc = null;
for (String authenticationMethod : authList) {
try {
return RegistryAuthenticator.fromAuthenticationMethod(
authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient);
} catch (RegistryAuthenticationFailedException ex) {
if (lastExc == null) {
lastExc =
new RegistryErrorExceptionBuilder(getActionDescription(), ex)
.addReason(
"Failed getting supported authentication method from 'WWW-Authenticate' header")
.build();
}
}
}

// if none of the RegistryAuthenticators worked, throw the last stored exception
throw lastExc;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,40 @@ private <T> T callRegistryEndpoint(RegistryEndpointProvider<T> registryEndpointP

// Because we successfully did bearer authentication initially, getting 401 here probably
// means the token was expired.
String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate();
authorization.set(refreshBearerAuth(wwwAuthenticate));
// There may be multiple WWW-Authenticate headers, so we iterate them until
// refreshBearerAuth() succeeds
List<String> wwwAuthenticateList =
ex.getHttpResponseException().getHeaders().getAuthenticateAsList();
Authorization auth = null;
if (wwwAuthenticateList != null && !wwwAuthenticateList.isEmpty()) {
Exception storedEx = null;
// try all WWW-Authenticate headers until one succeeds
for (String wwwAuthenticate : wwwAuthenticateList) {
try {
storedEx = null;
auth = refreshBearerAuth(wwwAuthenticate);
break;
} catch (Exception exc) {
storedEx = exc;
}
}
// if none of the headers worked, throw the last exception that occurred
if (storedEx != null) {
if (storedEx instanceof IllegalStateException) {
throw (IllegalStateException) storedEx;
} else if (storedEx instanceof RegistryException) {
throw (RegistryException) storedEx;
} else {
throw new IllegalStateException(
"unexpected exception during handling of WWW-Authenticate headers: " + storedEx);
}
}
}
// if no WWW-Authenticate header was provided, perform a refreshBearerAuth without it anyway
if (auth == null) {
auth = refreshBearerAuth(null);
}
authorization.set(auth);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.cloud.tools.jib.http.FailoverHttpClient;
import com.google.cloud.tools.jib.http.Response;
import com.google.cloud.tools.jib.http.ResponseException;
import com.google.common.collect.Lists;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
Expand Down Expand Up @@ -102,7 +103,7 @@ public void testHandleHttpResponseException_noHeader() throws ResponseException
Mockito.when(mockResponseException.getStatusCode())
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(null);
Mockito.when(mockHeaders.getAuthenticateAsList()).thenReturn(null);

try {
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException);
Expand All @@ -122,7 +123,8 @@ public void testHandleHttpResponseException_badAuthenticationMethod() throws Res
Mockito.when(mockResponseException.getStatusCode())
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod);
Mockito.when(mockHeaders.getAuthenticateAsList())
.thenReturn(Lists.newArrayList(authenticationMethod));

try {
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException);
Expand All @@ -133,7 +135,7 @@ public void testHandleHttpResponseException_badAuthenticationMethod() throws Res
MatcherAssert.assertThat(
ex.getMessage(),
CoreMatchers.containsString(
"Failed get authentication method from 'WWW-Authenticate' header"));
"Failed getting supported authentication method from 'WWW-Authenticate' header"));
}
}

Expand All @@ -146,7 +148,30 @@ public void testHandleHttpResponseException_pass()
Mockito.when(mockResponseException.getStatusCode())
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod);
Mockito.when(mockHeaders.getAuthenticateAsList())
.thenReturn(Lists.newArrayList(authenticationMethod));

RegistryAuthenticator registryAuthenticator =
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get();

Assert.assertEquals(
new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"),
registryAuthenticator.getAuthenticationUrl(
null, Collections.singletonMap("someImageName", "someScope")));
}

@Test
public void testHandleHttpResponseExceptionWithKerberosFirst_pass()
throws RegistryErrorException, ResponseException, MalformedURLException {
String authenticationMethodNegotiate = "Negotiate";
String authenticationMethodBearer =
"Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"";

Mockito.when(mockResponseException.getStatusCode())
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
Mockito.when(mockHeaders.getAuthenticateAsList())
.thenReturn(Lists.newArrayList(authenticationMethodNegotiate, authenticationMethodBearer));

RegistryAuthenticator registryAuthenticator =
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get();
Expand Down