Skip to content

Commit b7eb512

Browse files
committed
fix: primitive support for multiple WWW-Authenticate response headers (#4187)
* fix: try all authenticate methods from the `WWW-Authenticate` response header until one succeeds
1 parent 0df8ed9 commit b7eb512

File tree

4 files changed

+109
-28
lines changed

4 files changed

+109
-28
lines changed

Diff for: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java

+26-11
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,30 @@ public ImagesAndRegistryClient call()
171171
.setCredential(credential)
172172
.newRegistryClient();
173173

174-
String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate();
175-
if (wwwAuthenticate != null) {
176-
eventHandlers.dispatch(
177-
LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate));
178-
registryClient.authPullByWwwAuthenticate(wwwAuthenticate);
179-
return new ImagesAndRegistryClient(
180-
pullBaseImages(registryClient, progressDispatcher.newChildProducer()),
181-
registryClient);
174+
List<String> wwwAuthenticateList =
175+
ex.getHttpResponseException().getHeaders().getAuthenticateAsList();
176+
if (wwwAuthenticateList != null && !wwwAuthenticateList.isEmpty()) {
177+
RegistryException storedEx = null;
178+
// try all WWW-Authenticate headers until one succeeds
179+
for (String wwwAuthenticate : wwwAuthenticateList) {
180+
eventHandlers.dispatch(
181+
LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate));
182+
try {
183+
storedEx = null;
184+
registryClient.authPullByWwwAuthenticate(wwwAuthenticate);
185+
break;
186+
} catch (RegistryException exc) {
187+
eventHandlers.dispatch(
188+
LogEvent.debug(
189+
"WWW-Authenticate failed for " + imageReference + ": " + wwwAuthenticate));
190+
storedEx = exc;
191+
}
192+
}
193+
// if none of the WWW-Authenticate headers worked for the authPullByWwwAuthenticate, throw
194+
// the last stored exception
195+
if (storedEx != null) {
196+
throw storedEx;
197+
}
182198

183199
} else {
184200
// Not getting WWW-Authenticate is unexpected in practice, and we may just blame the
@@ -200,10 +216,9 @@ public ImagesAndRegistryClient call()
200216
eventHandlers.dispatch(
201217
LogEvent.debug("Trying bearer auth as fallback for " + imageReference + "..."));
202218
registryClient.doPullBearerAuth();
203-
return new ImagesAndRegistryClient(
204-
pullBaseImages(registryClient, progressDispatcher.newChildProducer()),
205-
registryClient);
206219
}
220+
return new ImagesAndRegistryClient(
221+
pullBaseImages(registryClient, progressDispatcher.newChildProducer()), registryClient);
207222
}
208223
}
209224
}

Diff for: jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java

+20-11
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,31 @@ public Optional<RegistryAuthenticator> handleHttpResponseException(
9393
}
9494

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

103-
// Parses the header to retrieve the components.
104-
try {
105-
return RegistryAuthenticator.fromAuthenticationMethod(
106-
authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient);
107-
108-
} catch (RegistryAuthenticationFailedException ex) {
109-
throw new RegistryErrorExceptionBuilder(getActionDescription(), ex)
110-
.addReason("Failed get authentication method from 'WWW-Authenticate' header")
111-
.build();
103+
// try all 'WWW-Authenticate' headers until a working RegistryAuthenticator can be created
104+
RegistryErrorException lastExc = null;
105+
for (String authenticationMethod : authList) {
106+
try {
107+
return RegistryAuthenticator.fromAuthenticationMethod(
108+
authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient);
109+
} catch (RegistryAuthenticationFailedException ex) {
110+
if (lastExc == null) {
111+
lastExc =
112+
new RegistryErrorExceptionBuilder(getActionDescription(), ex)
113+
.addReason(
114+
"Failed getting supported authentication method from 'WWW-Authenticate' header")
115+
.build();
116+
}
117+
}
112118
}
119+
120+
// if none of the RegistryAuthenticators worked, throw the last stored exception
121+
throw lastExc;
113122
}
114123
}

Diff for: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java

+34-2
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,40 @@ private <T> T callRegistryEndpoint(RegistryEndpointProvider<T> registryEndpointP
632632

633633
// Because we successfully did bearer authentication initially, getting 401 here probably
634634
// means the token was expired.
635-
String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate();
636-
authorization.set(refreshBearerAuth(wwwAuthenticate));
635+
// There may be multiple WWW-Authenticate headers, so we iterate them until
636+
// refreshBearerAuth() succeeds
637+
List<String> wwwAuthenticateList =
638+
ex.getHttpResponseException().getHeaders().getAuthenticateAsList();
639+
Authorization auth = null;
640+
if (wwwAuthenticateList != null && !wwwAuthenticateList.isEmpty()) {
641+
Exception storedEx = null;
642+
// try all WWW-Authenticate headers until one succeeds
643+
for (String wwwAuthenticate : wwwAuthenticateList) {
644+
try {
645+
storedEx = null;
646+
auth = refreshBearerAuth(wwwAuthenticate);
647+
break;
648+
} catch (Exception exc) {
649+
storedEx = exc;
650+
}
651+
}
652+
// if none of the headers worked, throw the last exception that occurred
653+
if (storedEx != null) {
654+
if (storedEx instanceof IllegalStateException) {
655+
throw (IllegalStateException) storedEx;
656+
} else if (storedEx instanceof RegistryException) {
657+
throw (RegistryException) storedEx;
658+
} else {
659+
throw new IllegalStateException(
660+
"unexpected exception during handling of WWW-Authenticate headers: " + storedEx);
661+
}
662+
}
663+
}
664+
// if no WWW-Authenticate header was provided, perform a refreshBearerAuth without it anyway
665+
if (auth == null) {
666+
auth = refreshBearerAuth(null);
667+
}
668+
authorization.set(auth);
637669
}
638670
}
639671
}

Diff for: jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java

+29-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.cloud.tools.jib.http.FailoverHttpClient;
2323
import com.google.cloud.tools.jib.http.Response;
2424
import com.google.cloud.tools.jib.http.ResponseException;
25+
import com.google.common.collect.Lists;
2526
import java.net.MalformedURLException;
2627
import java.net.URL;
2728
import java.util.Collections;
@@ -102,7 +103,7 @@ public void testHandleHttpResponseException_noHeader() throws ResponseException
102103
Mockito.when(mockResponseException.getStatusCode())
103104
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
104105
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
105-
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(null);
106+
Mockito.when(mockHeaders.getAuthenticateAsList()).thenReturn(null);
106107

107108
try {
108109
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException);
@@ -122,7 +123,8 @@ public void testHandleHttpResponseException_badAuthenticationMethod() throws Res
122123
Mockito.when(mockResponseException.getStatusCode())
123124
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
124125
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
125-
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod);
126+
Mockito.when(mockHeaders.getAuthenticateAsList())
127+
.thenReturn(Lists.newArrayList(authenticationMethod));
126128

127129
try {
128130
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException);
@@ -133,7 +135,7 @@ public void testHandleHttpResponseException_badAuthenticationMethod() throws Res
133135
MatcherAssert.assertThat(
134136
ex.getMessage(),
135137
CoreMatchers.containsString(
136-
"Failed get authentication method from 'WWW-Authenticate' header"));
138+
"Failed getting supported authentication method from 'WWW-Authenticate' header"));
137139
}
138140
}
139141

@@ -146,7 +148,30 @@ public void testHandleHttpResponseException_pass()
146148
Mockito.when(mockResponseException.getStatusCode())
147149
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
148150
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
149-
Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod);
151+
Mockito.when(mockHeaders.getAuthenticateAsList())
152+
.thenReturn(Lists.newArrayList(authenticationMethod));
153+
154+
RegistryAuthenticator registryAuthenticator =
155+
testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get();
156+
157+
Assert.assertEquals(
158+
new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"),
159+
registryAuthenticator.getAuthenticationUrl(
160+
null, Collections.singletonMap("someImageName", "someScope")));
161+
}
162+
163+
@Test
164+
public void testHandleHttpResponseExceptionWithKerberosFirst_pass()
165+
throws RegistryErrorException, ResponseException, MalformedURLException {
166+
String authenticationMethodNegotiate = "Negotiate";
167+
String authenticationMethodBearer =
168+
"Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\"";
169+
170+
Mockito.when(mockResponseException.getStatusCode())
171+
.thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
172+
Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders);
173+
Mockito.when(mockHeaders.getAuthenticateAsList())
174+
.thenReturn(Lists.newArrayList(authenticationMethodNegotiate, authenticationMethodBearer));
150175

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

0 commit comments

Comments
 (0)