1 /* 2 * Copyright 2021 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.oauth2; 33 34 import static com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE; 35 import static org.junit.Assert.assertEquals; 36 import static org.junit.Assert.assertNotNull; 37 import static org.junit.Assert.fail; 38 39 import com.google.api.client.http.HttpTransport; 40 import com.google.auth.TestUtils; 41 import com.google.auth.http.HttpTransportFactory; 42 import java.io.IOException; 43 import java.util.Date; 44 import java.util.Map; 45 import org.junit.Test; 46 import org.junit.runner.RunWith; 47 import org.junit.runners.JUnit4; 48 49 /** Tests for {@link DownscopedCredentials}. */ 50 @RunWith(JUnit4.class) 51 public class DownscopedCredentialsTest { 52 53 private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token"; 54 private static final String SA_PRIVATE_KEY_PKCS8 = 55 "-----BEGIN PRIVATE KEY-----\n" 56 + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" 57 + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0" 58 + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw" 59 + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr" 60 + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6" 61 + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP" 62 + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut" 63 + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA" 64 + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ" 65 + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ" 66 + "==\n-----END PRIVATE KEY-----\n"; 67 68 private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY = 69 CredentialAccessBoundary.newBuilder() 70 .addRule( 71 CredentialAccessBoundary.AccessBoundaryRule.newBuilder() 72 .setAvailableResource("//storage.googleapis.com/projects/_/buckets/bucket") 73 .addAvailablePermission("inRole:roles/storage.objectViewer") 74 .build()) 75 .build(); 76 77 static class MockStsTransportFactory implements HttpTransportFactory { 78 79 MockStsTransport transport = new MockStsTransport(); 80 81 @Override create()82 public HttpTransport create() { 83 return transport; 84 } 85 } 86 87 @Test refreshAccessToken()88 public void refreshAccessToken() throws IOException { 89 MockStsTransportFactory transportFactory = new MockStsTransportFactory(); 90 91 GoogleCredentials sourceCredentials = 92 getServiceAccountSourceCredentials(/* canRefresh= */ true); 93 94 DownscopedCredentials downscopedCredentials = 95 DownscopedCredentials.newBuilder() 96 .setSourceCredential(sourceCredentials) 97 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 98 .setHttpTransportFactory(transportFactory) 99 .build(); 100 101 AccessToken accessToken = downscopedCredentials.refreshAccessToken(); 102 103 assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); 104 105 // Validate CAB specific params. 106 Map<String, String> query = 107 TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString()); 108 assertNotNull(query.get("options")); 109 assertEquals(CREDENTIAL_ACCESS_BOUNDARY.toJson(), query.get("options")); 110 assertEquals( 111 "urn:ietf:params:oauth:token-type:access_token", query.get("requested_token_type")); 112 113 // Verify domain. 114 String url = transportFactory.transport.getRequest().getUrl(); 115 assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, GOOGLE_DEFAULT_UNIVERSE)); 116 } 117 118 @Test refreshAccessToken_withCustomUniverseDomain()119 public void refreshAccessToken_withCustomUniverseDomain() throws IOException { 120 MockStsTransportFactory transportFactory = new MockStsTransportFactory(); 121 String universeDomain = "foobar"; 122 GoogleCredentials sourceCredentials = 123 getServiceAccountSourceCredentials(/* canRefresh= */ true) 124 .toBuilder() 125 .setUniverseDomain(universeDomain) 126 .build(); 127 128 DownscopedCredentials downscopedCredentials = 129 DownscopedCredentials.newBuilder() 130 .setSourceCredential(sourceCredentials) 131 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 132 .setHttpTransportFactory(transportFactory) 133 .setUniverseDomain(universeDomain) 134 .build(); 135 136 AccessToken accessToken = downscopedCredentials.refreshAccessToken(); 137 138 assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); 139 140 // Validate CAB specific params. 141 Map<String, String> query = 142 TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString()); 143 assertNotNull(query.get("options")); 144 assertEquals(CREDENTIAL_ACCESS_BOUNDARY.toJson(), query.get("options")); 145 assertEquals( 146 "urn:ietf:params:oauth:token-type:access_token", query.get("requested_token_type")); 147 148 // Verify domain. 149 String url = transportFactory.transport.getRequest().getUrl(); 150 assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain)); 151 } 152 153 @Test refreshAccessToken_userCredentials_expectExpiresInCopied()154 public void refreshAccessToken_userCredentials_expectExpiresInCopied() throws IOException { 155 // STS only returns expires_in if the source access token belongs to a service account. 156 // For other source credential types, we can copy the source credentials expiration as 157 // the generated downscoped token will always have the same expiration time as the source 158 // credentials. 159 160 MockStsTransportFactory transportFactory = new MockStsTransportFactory(); 161 transportFactory.transport.setReturnExpiresIn(false); 162 163 GoogleCredentials sourceCredentials = getUserSourceCredentials(); 164 165 DownscopedCredentials downscopedCredentials = 166 DownscopedCredentials.newBuilder() 167 .setSourceCredential(sourceCredentials) 168 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 169 .setHttpTransportFactory(transportFactory) 170 .build(); 171 172 AccessToken accessToken = downscopedCredentials.refreshAccessToken(); 173 174 assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue()); 175 176 // Validate that the expires_in has been copied from the source credential. 177 assertEquals( 178 sourceCredentials.getAccessToken().getExpirationTime(), accessToken.getExpirationTime()); 179 } 180 181 @Test refreshAccessToken_cantRefreshSourceCredentials_throws()182 public void refreshAccessToken_cantRefreshSourceCredentials_throws() throws IOException { 183 MockStsTransportFactory transportFactory = new MockStsTransportFactory(); 184 185 GoogleCredentials sourceCredentials = 186 getServiceAccountSourceCredentials(/* canRefresh= */ false); 187 188 DownscopedCredentials downscopedCredentials = 189 DownscopedCredentials.newBuilder() 190 .setSourceCredential(sourceCredentials) 191 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 192 .setHttpTransportFactory(transportFactory) 193 .build(); 194 195 try { 196 downscopedCredentials.refreshAccessToken(); 197 fail("Should fail as the source credential should not be able to be refreshed."); 198 } catch (IOException e) { 199 assertEquals("Unable to refresh the provided source credential.", e.getMessage()); 200 } 201 } 202 203 @Test builder_noSourceCredential_throws()204 public void builder_noSourceCredential_throws() { 205 try { 206 DownscopedCredentials.newBuilder() 207 .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) 208 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 209 .build(); 210 fail("Should fail as the source credential is null."); 211 } catch (NullPointerException e) { 212 // Expected. 213 } 214 } 215 216 @Test builder_noCredentialAccessBoundary_throws()217 public void builder_noCredentialAccessBoundary_throws() throws IOException { 218 try { 219 DownscopedCredentials.newBuilder() 220 .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) 221 .setSourceCredential(getServiceAccountSourceCredentials(/* canRefresh= */ true)) 222 .build(); 223 fail("Should fail as no access boundary was provided."); 224 } catch (NullPointerException e) { 225 // Expected. 226 } 227 } 228 229 @Test builder_noTransport_defaults()230 public void builder_noTransport_defaults() throws IOException { 231 GoogleCredentials sourceCredentials = 232 getServiceAccountSourceCredentials(/* canRefresh= */ true); 233 DownscopedCredentials credentials = 234 DownscopedCredentials.newBuilder() 235 .setSourceCredential(sourceCredentials) 236 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 237 .build(); 238 239 GoogleCredentials scopedSourceCredentials = 240 sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform"); 241 assertEquals(scopedSourceCredentials, credentials.getSourceCredentials()); 242 assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary()); 243 assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory()); 244 } 245 246 @Test builder_noUniverseDomain_defaults()247 public void builder_noUniverseDomain_defaults() throws IOException { 248 GoogleCredentials sourceCredentials = 249 getServiceAccountSourceCredentials(/* canRefresh= */ true); 250 DownscopedCredentials credentials = 251 DownscopedCredentials.newBuilder() 252 .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) 253 .setSourceCredential(sourceCredentials) 254 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 255 .build(); 256 257 GoogleCredentials scopedSourceCredentials = 258 sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform"); 259 assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory()); 260 assertEquals(scopedSourceCredentials, credentials.getSourceCredentials()); 261 assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary()); 262 assertEquals(GOOGLE_DEFAULT_UNIVERSE, credentials.getUniverseDomain()); 263 } 264 265 @Test builder_universeDomainMismatch_throws()266 public void builder_universeDomainMismatch_throws() throws IOException { 267 GoogleCredentials sourceCredentials = 268 getServiceAccountSourceCredentials(/* canRefresh= */ true); 269 270 try { 271 DownscopedCredentials.newBuilder() 272 .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) 273 .setSourceCredential(sourceCredentials) 274 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 275 .setUniverseDomain("differentUniverseDomain") 276 .build(); 277 fail("Should fail with universe domain mismatch."); 278 } catch (IllegalArgumentException e) { 279 assertEquals( 280 "The downscoped credential's universe domain must be the same as the source credential.", 281 e.getMessage()); 282 } 283 } 284 285 @Test builder_sourceUniverseDomainUnavailable_throws()286 public void builder_sourceUniverseDomainUnavailable_throws() throws IOException { 287 GoogleCredentials sourceCredentials = new MockSourceCredentialWithoutUniverseDomain(); 288 289 try { 290 DownscopedCredentials.newBuilder() 291 .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY) 292 .setSourceCredential(sourceCredentials) 293 .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY) 294 .build(); 295 fail("Should fail to retrieve source credential universe domain."); 296 } catch (IllegalStateException e) { 297 assertEquals( 298 "Error occurred when attempting to retrieve source credential universe domain.", 299 e.getMessage()); 300 } 301 } 302 getServiceAccountSourceCredentials(boolean canRefresh)303 private static GoogleCredentials getServiceAccountSourceCredentials(boolean canRefresh) 304 throws IOException { 305 MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); 306 307 String email = "service-account@google.com"; 308 309 ServiceAccountCredentials sourceCredentials = 310 ServiceAccountCredentials.newBuilder() 311 .setClientEmail(email) 312 .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8)) 313 .setPrivateKeyId("privateKeyId") 314 .setProjectId("projectId") 315 .setHttpTransportFactory(transportFactory) 316 .build(); 317 318 transportFactory.transport.addServiceAccount(email, "accessToken"); 319 320 if (!canRefresh) { 321 transportFactory.transport.setError(new IOException()); 322 } 323 324 return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform"); 325 } 326 getUserSourceCredentials()327 private static GoogleCredentials getUserSourceCredentials() { 328 MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory(); 329 transportFactory.transport.addClient("clientId", "clientSecret"); 330 transportFactory.transport.addRefreshToken("refreshToken", "accessToken"); 331 AccessToken accessToken = new AccessToken("accessToken", new Date()); 332 return UserCredentials.newBuilder() 333 .setClientId("clientId") 334 .setClientSecret("clientSecret") 335 .setRefreshToken("refreshToken") 336 .setAccessToken(accessToken) 337 .setHttpTransportFactory(transportFactory) 338 .build(); 339 } 340 341 static class MockSourceCredentialWithoutUniverseDomain extends GoogleCredentials { 342 @Override getUniverseDomain()343 public String getUniverseDomain() throws IOException { 344 throw new IOException(); 345 } 346 } 347 } 348