Skip to content

Commit 8451d05

Browse files
committed
Add unit tests for the builder and refreshCredentials()
1 parent eb4b793 commit 8451d05

File tree

4 files changed

+348
-2
lines changed

4 files changed

+348
-2
lines changed

cab-token-generator/java/com/google/auth/credentialaccessboundary/ClientSideCredentialAccessBoundaryFactory.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.auth.oauth2.StsRequestHandler;
4646
import com.google.auth.oauth2.StsTokenExchangeRequest;
4747
import com.google.auth.oauth2.StsTokenExchangeResponse;
48+
import com.google.common.annotations.VisibleForTesting;
4849
import com.google.common.base.Strings;
4950
import com.google.common.util.concurrent.SettableFuture;
5051
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -98,7 +99,8 @@ private ClientSideCredentialAccessBoundaryFactory(Builder builder) {
9899
*
99100
* @throws IOException If an error occurs during credential refresh or token exchange.
100101
*/
101-
private void refreshCredentials() throws IOException {
102+
@VisibleForTesting
103+
void refreshCredentials() throws IOException {
102104
try {
103105
// Force a refresh on the source credentials. The intermediate token's lifetime is tied to the
104106
// source credential's expiration. The factory's refreshMargin might be different from the
@@ -358,4 +360,24 @@ public ClientSideCredentialAccessBoundaryFactory build() {
358360
return new ClientSideCredentialAccessBoundaryFactory(this);
359361
}
360362
}
363+
364+
@VisibleForTesting
365+
String getAccessBoundarySessionKey() {
366+
return accessBoundarySessionKey;
367+
}
368+
369+
@VisibleForTesting
370+
AccessToken getIntermediateAccessToken() {
371+
return intermediateAccessToken;
372+
}
373+
374+
@VisibleForTesting
375+
String getTokenExchangeEndpoint() {
376+
return tokenExchangeEndpoint;
377+
}
378+
379+
@VisibleForTesting
380+
HttpTransportFactory getTransportFactory() {
381+
return transportFactory;
382+
}
361383
}
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
/*
2+
* Copyright 2024, Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.credentialaccessboundary;
33+
34+
import static com.google.auth.oauth2.OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT;
35+
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertThrows;
37+
import static org.junit.Assert.fail;
38+
39+
import com.google.api.client.http.HttpTransport;
40+
import com.google.auth.Credentials;
41+
import com.google.auth.TestUtils;
42+
import com.google.auth.http.HttpTransportFactory;
43+
import com.google.auth.oauth2.AccessToken;
44+
import com.google.auth.oauth2.GoogleCredentials;
45+
import com.google.auth.oauth2.MockStsTransport;
46+
import com.google.auth.oauth2.MockTokenServerTransportFactory;
47+
import com.google.auth.oauth2.OAuth2Utils;
48+
import com.google.auth.oauth2.ServiceAccountCredentials;
49+
import java.io.IOException;
50+
import java.time.Duration;
51+
import java.util.Map;
52+
import java.util.Objects;
53+
import org.junit.Test;
54+
import org.junit.runner.RunWith;
55+
import org.junit.runners.JUnit4;
56+
57+
/** Tests for {@link com.google.auth.oauth2.DownscopedCredentials}. */
58+
@RunWith(JUnit4.class)
59+
public class ClientSideCredentialAccessBoundaryFactoryTest {
60+
private static final String SA_PRIVATE_KEY_PKCS8 =
61+
"-----BEGIN PRIVATE KEY-----\n"
62+
+ "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
63+
+ "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
64+
+ "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
65+
+ "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
66+
+ "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
67+
+ "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
68+
+ "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
69+
+ "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
70+
+ "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
71+
+ "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
72+
+ "==\n-----END PRIVATE KEY-----\n";
73+
74+
static class MockStsTransportFactory implements HttpTransportFactory {
75+
76+
MockStsTransport transport = new MockStsTransport();
77+
78+
@Override
79+
public HttpTransport create() {
80+
return transport;
81+
}
82+
}
83+
84+
@Test
85+
public void refreshCredentials() throws Exception {
86+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
87+
transportFactory.transport.setReturnAccessBoundarySessionKey(true);
88+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
89+
90+
ClientSideCredentialAccessBoundaryFactory factory =
91+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
92+
.setSourceCredential(sourceCredentials)
93+
.setHttpTransportFactory(transportFactory)
94+
.build();
95+
96+
factory.refreshCredentials();
97+
98+
// Verify requested token type.
99+
Map<String, String> query =
100+
TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString());
101+
assertEquals(
102+
OAuth2Utils.TOKEN_TYPE_ACCESS_BOUNDARY_INTERMEDIARY_TOKEN,
103+
query.get("requested_token_type"));
104+
105+
// Verify intermediate token and session key.
106+
AccessToken intermediateAccessToken = factory.getIntermediateAccessToken();
107+
String accessBoundarySessionKey = factory.getAccessBoundarySessionKey();
108+
assertEquals(
109+
transportFactory.transport.getAccessBoundarySessionKey(), accessBoundarySessionKey);
110+
assertEquals(
111+
transportFactory.transport.getAccessToken(), intermediateAccessToken.getTokenValue());
112+
}
113+
114+
@Test
115+
public void refreshCredentials_withCustomUniverseDomain() throws IOException {
116+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
117+
String universeDomain = "foobar";
118+
GoogleCredentials sourceCredentials =
119+
getServiceAccountSourceCredentials(/* canRefresh= */ true)
120+
.toBuilder()
121+
.setUniverseDomain(universeDomain)
122+
.build();
123+
124+
ClientSideCredentialAccessBoundaryFactory factory =
125+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
126+
.setUniverseDomain(universeDomain)
127+
.setSourceCredential(sourceCredentials)
128+
.setHttpTransportFactory(transportFactory)
129+
.build();
130+
131+
factory.refreshCredentials();
132+
133+
// Verify domain.
134+
String url = transportFactory.transport.getRequest().getUrl();
135+
assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain));
136+
}
137+
138+
@Test
139+
public void refreshCredentials_sourceCredentialCannotRefresh_throwsIOException()
140+
throws Exception {
141+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
142+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(false);
143+
144+
ClientSideCredentialAccessBoundaryFactory factory =
145+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
146+
.setSourceCredential(sourceCredentials)
147+
.setHttpTransportFactory(transportFactory)
148+
.build();
149+
150+
try {
151+
factory.refreshCredentials(); // Expecting an IOException
152+
fail("Should fail as the source credential should not be able to be refreshed.");
153+
} catch (IOException e) {
154+
assertEquals("Unable to refresh the provided source credential.", e.getMessage());
155+
}
156+
}
157+
158+
@Test
159+
public void refreshCredentials_noExpiresInReturned_copiesSourceExpiration() throws Exception {
160+
161+
MockStsTransportFactory transportFactory = new MockStsTransportFactory();
162+
transportFactory.transport.setReturnExpiresIn(false); // Simulate STS not returning expires_in
163+
164+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
165+
166+
ClientSideCredentialAccessBoundaryFactory factory =
167+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
168+
.setSourceCredential(sourceCredentials)
169+
.setHttpTransportFactory(transportFactory)
170+
.build();
171+
172+
factory.refreshCredentials();
173+
AccessToken intermediateAccessToken = factory.getIntermediateAccessToken();
174+
175+
assertEquals(
176+
transportFactory.transport.getAccessToken(), intermediateAccessToken.getTokenValue());
177+
178+
// Validate that the expires_in has been copied from the source credential.
179+
assertEquals(
180+
Objects.requireNonNull(sourceCredentials.getAccessToken()).getExpirationTime(),
181+
intermediateAccessToken.getExpirationTime());
182+
}
183+
184+
/** Tests for {@link ClientSideCredentialAccessBoundaryFactory.Builder}. */
185+
public static class BuilderTest {
186+
@Test
187+
public void builder_noSourceCredential_throws() {
188+
try {
189+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
190+
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
191+
.build();
192+
fail("Should fail as the source credential is null.");
193+
} catch (NullPointerException e) {
194+
assertEquals("Source credential must not be null.", e.getMessage());
195+
}
196+
}
197+
198+
@Test
199+
public void builder_minimumTokenLifetime_negative_throws() throws IOException {
200+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
201+
IllegalArgumentException exception =
202+
assertThrows(
203+
IllegalArgumentException.class,
204+
() ->
205+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
206+
.setSourceCredential(sourceCredentials)
207+
.setMinimumTokenLifetime(Duration.ofMinutes(-1)));
208+
209+
assertEquals("Minimum token lifetime must be positive.", exception.getMessage());
210+
}
211+
212+
@Test
213+
public void builder_minimumTokenLifetime_zero_throws() throws IOException {
214+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
215+
IllegalArgumentException exception =
216+
assertThrows(
217+
IllegalArgumentException.class,
218+
() ->
219+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
220+
.setSourceCredential(sourceCredentials)
221+
.setMinimumTokenLifetime(Duration.ZERO));
222+
223+
assertEquals("Minimum token lifetime must be positive.", exception.getMessage());
224+
}
225+
226+
@Test
227+
public void builder_refreshMargin_negative_throws() throws IOException {
228+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
229+
IllegalArgumentException exception =
230+
assertThrows(
231+
IllegalArgumentException.class,
232+
() ->
233+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
234+
.setSourceCredential(sourceCredentials)
235+
.setRefreshMargin(Duration.ofMinutes(-1)));
236+
237+
assertEquals("Refresh margin must be positive.", exception.getMessage());
238+
}
239+
240+
@Test
241+
public void builder_refreshMargin_zero_throws() throws IOException {
242+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
243+
IllegalArgumentException exception =
244+
assertThrows(
245+
IllegalArgumentException.class,
246+
() ->
247+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
248+
.setSourceCredential(sourceCredentials)
249+
.setRefreshMargin(Duration.ZERO));
250+
251+
assertEquals("Refresh margin must be positive.", exception.getMessage());
252+
}
253+
254+
@Test
255+
public void builder_setsCorrectDefaultValues() throws IOException {
256+
GoogleCredentials sourceCredentials = getServiceAccountSourceCredentials(true);
257+
ClientSideCredentialAccessBoundaryFactory factory =
258+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
259+
.setSourceCredential(sourceCredentials)
260+
.build();
261+
262+
assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, factory.getTransportFactory());
263+
assertEquals(
264+
String.format(OAuth2Utils.TOKEN_EXCHANGE_URL_FORMAT, Credentials.GOOGLE_DEFAULT_UNIVERSE),
265+
factory.getTokenExchangeEndpoint());
266+
}
267+
268+
@Test
269+
public void builder_universeDomainMismatch_throws() throws IOException {
270+
GoogleCredentials sourceCredentials =
271+
getServiceAccountSourceCredentials(/* canRefresh= */ true);
272+
273+
try {
274+
ClientSideCredentialAccessBoundaryFactory.newBuilder()
275+
.setSourceCredential(sourceCredentials)
276+
.setUniverseDomain("differentUniverseDomain")
277+
.build();
278+
279+
fail("Should fail with universe domain mismatch.");
280+
} catch (IllegalArgumentException e) {
281+
assertEquals(
282+
"The client side access boundary credential's universe domain must be the same as the source credential.",
283+
e.getMessage());
284+
}
285+
}
286+
}
287+
288+
private static GoogleCredentials getServiceAccountSourceCredentials(boolean canRefresh)
289+
throws IOException {
290+
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
291+
292+
String email = "[email protected]";
293+
294+
ServiceAccountCredentials sourceCredentials =
295+
ServiceAccountCredentials.newBuilder()
296+
.setClientEmail(email)
297+
.setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
298+
.setPrivateKeyId("privateKeyId")
299+
.setProjectId("projectId")
300+
.setHttpTransportFactory(transportFactory)
301+
.build();
302+
303+
transportFactory.transport.addServiceAccount(email, "accessToken");
304+
305+
if (!canRefresh) {
306+
transportFactory.transport.setError(new IOException());
307+
}
308+
309+
return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
310+
}
311+
}

cab-token-generator/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
<build>
1717
<sourceDirectory>java</sourceDirectory>
18+
<testSourceDirectory>javatests</testSourceDirectory>
1819
</build>
1920
<dependencies>
2021
<dependency>
@@ -37,6 +38,18 @@
3738
<groupId>com.google.guava</groupId>
3839
<artifactId>guava</artifactId>
3940
</dependency>
41+
<dependency>
42+
<groupId>junit</groupId>
43+
<artifactId>junit</artifactId>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>com.google.auth</groupId>
48+
<artifactId>google-auth-library-oauth2-http</artifactId>
49+
<scope>test</scope>
50+
<type>test-jar</type>
51+
<classifier>testlib</classifier>
52+
</dependency>
4053
</dependencies>
4154

4255
</project>

oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ static Map<String, Object> validateMap(Map<String, Object> map, String key, Stri
257257
}
258258

259259
/** Helper to convert from a PKCS#8 String to an RSA private key */
260-
static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
260+
public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
261261
Reader reader = new StringReader(privateKeyPkcs8);
262262
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
263263
if (section == null) {

0 commit comments

Comments
 (0)