Skip to content

Commit

Permalink
fix: primitive support for multiple WWW-Authenticate response heade…
Browse files Browse the repository at this point in the history
…rs (#4187)

* fix: try all authenticate methods from the `WWW-Authenticate` response header until one succeeds
  • Loading branch information
kosi2801 committed Feb 16, 2024
1 parent 0df8ed9 commit b7eb512
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 28 deletions.
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

0 comments on commit b7eb512

Please sign in to comment.