1 /* 2 * Copyright 2015, Google Inc. All rights reserved. 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 Inc. 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.oauth2.GoogleCredentials.SERVICE_ACCOUNT_FILE_TYPE; 35 import static com.google.auth.oauth2.GoogleCredentials.addQuotaProjectIdToRequestMetadata; 36 37 import com.google.api.client.json.GenericJson; 38 import com.google.api.client.json.JsonFactory; 39 import com.google.api.client.json.JsonObjectParser; 40 import com.google.api.client.util.Clock; 41 import com.google.api.client.util.Preconditions; 42 import com.google.auth.Credentials; 43 import com.google.auth.RequestMetadataCallback; 44 import com.google.auth.ServiceAccountSigner; 45 import com.google.common.annotations.VisibleForTesting; 46 import com.google.common.base.MoreObjects; 47 import com.google.common.base.Throwables; 48 import com.google.common.base.Ticker; 49 import com.google.common.cache.CacheBuilder; 50 import com.google.common.cache.CacheLoader; 51 import com.google.common.cache.LoadingCache; 52 import com.google.common.util.concurrent.UncheckedExecutionException; 53 import com.google.errorprone.annotations.CanIgnoreReturnValue; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.ObjectInputStream; 57 import java.net.URI; 58 import java.nio.charset.StandardCharsets; 59 import java.security.InvalidKeyException; 60 import java.security.NoSuchAlgorithmException; 61 import java.security.PrivateKey; 62 import java.security.Signature; 63 import java.security.SignatureException; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Objects; 67 import java.util.concurrent.ExecutionException; 68 import java.util.concurrent.Executor; 69 import java.util.concurrent.TimeUnit; 70 71 /** 72 * Service Account credentials for calling Google APIs using a JWT directly for access. 73 * 74 * <p>Uses a JSON Web Token (JWT) directly in the request metadata to provide authorization. 75 */ 76 public class ServiceAccountJwtAccessCredentials extends Credentials 77 implements JwtProvider, ServiceAccountSigner, QuotaProjectIdProvider { 78 79 private static final long serialVersionUID = -7274955171379494197L; 80 static final String JWT_ACCESS_PREFIX = OAuth2Utils.BEARER_PREFIX; 81 82 @VisibleForTesting static final long LIFE_SPAN_SECS = TimeUnit.HOURS.toSeconds(1); 83 private static final long CLOCK_SKEW = TimeUnit.MINUTES.toSeconds(5); 84 85 private final String clientId; 86 private final String clientEmail; 87 private final PrivateKey privateKey; 88 private final String privateKeyId; 89 private final URI defaultAudience; 90 private final String quotaProjectId; 91 92 private transient LoadingCache<JwtClaims, JwtCredentials> credentialsCache; 93 94 // Until we expose this to the users it can remain transient and non-serializable 95 @VisibleForTesting transient Clock clock = Clock.SYSTEM; 96 97 /** 98 * Constructor with minimum identifying information. 99 * 100 * @param clientId Client ID of the service account from the console. May be null. 101 * @param clientEmail Client email address of the service account from the console. 102 * @param privateKey RSA private key object for the service account. 103 * @param privateKeyId Private key identifier for the service account. May be null. 104 */ ServiceAccountJwtAccessCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId)105 private ServiceAccountJwtAccessCredentials( 106 String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId) { 107 this(clientId, clientEmail, privateKey, privateKeyId, null, null); 108 } 109 110 /** 111 * Constructor with full information. 112 * 113 * @param clientId Client ID of the service account from the console. May be null. 114 * @param clientEmail Client email address of the service account from the console. 115 * @param privateKey RSA private key object for the service account. 116 * @param privateKeyId Private key identifier for the service account. May be null. 117 * @param defaultAudience Audience to use if not provided by transport. May be null. 118 */ ServiceAccountJwtAccessCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId, URI defaultAudience, String quotaProjectId)119 private ServiceAccountJwtAccessCredentials( 120 String clientId, 121 String clientEmail, 122 PrivateKey privateKey, 123 String privateKeyId, 124 URI defaultAudience, 125 String quotaProjectId) { 126 this.clientId = clientId; 127 this.clientEmail = Preconditions.checkNotNull(clientEmail); 128 this.privateKey = Preconditions.checkNotNull(privateKey); 129 this.privateKeyId = privateKeyId; 130 this.defaultAudience = defaultAudience; 131 this.credentialsCache = createCache(); 132 this.quotaProjectId = quotaProjectId; 133 } 134 135 /** 136 * Returns service account credentials defined by JSON using the format supported by the Google 137 * Developers Console. 138 * 139 * @param json a map from the JSON representing the credentials. 140 * @return the credentials defined by the JSON. 141 * @throws IOException if the credential cannot be created from the JSON. 142 */ fromJson(Map<String, Object> json)143 static ServiceAccountJwtAccessCredentials fromJson(Map<String, Object> json) throws IOException { 144 return fromJson(json, null); 145 } 146 147 /** 148 * Returns service account credentials defined by JSON using the format supported by the Google 149 * Developers Console. 150 * 151 * @param json a map from the JSON representing the credentials. 152 * @param defaultAudience Audience to use if not provided by transport. May be null. 153 * @return the credentials defined by the JSON. 154 * @throws IOException if the credential cannot be created from the JSON. 155 */ fromJson(Map<String, Object> json, URI defaultAudience)156 static ServiceAccountJwtAccessCredentials fromJson(Map<String, Object> json, URI defaultAudience) 157 throws IOException { 158 String clientId = (String) json.get("client_id"); 159 String clientEmail = (String) json.get("client_email"); 160 String privateKeyPkcs8 = (String) json.get("private_key"); 161 String privateKeyId = (String) json.get("private_key_id"); 162 String quoataProjectId = (String) json.get("quota_project_id"); 163 if (clientId == null 164 || clientEmail == null 165 || privateKeyPkcs8 == null 166 || privateKeyId == null) { 167 throw new IOException( 168 "Error reading service account credential from JSON, " 169 + "expecting 'client_id', 'client_email', 'private_key' and 'private_key_id'."); 170 } 171 return ServiceAccountJwtAccessCredentials.fromPkcs8( 172 clientId, clientEmail, privateKeyPkcs8, privateKeyId, defaultAudience, quoataProjectId); 173 } 174 175 /** 176 * Factory using PKCS#8 for the private key. 177 * 178 * @param clientId Client ID of the service account from the console. May be null. 179 * @param clientEmail Client email address of the service account from the console. 180 * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. 181 * @param privateKeyId Private key identifier for the service account. May be null. 182 * @return New ServiceAccountJwtAcceessCredentials created from a private key. 183 * @throws IOException if the credential cannot be created from the private key. 184 */ fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId)185 public static ServiceAccountJwtAccessCredentials fromPkcs8( 186 String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId) 187 throws IOException { 188 return fromPkcs8(clientId, clientEmail, privateKeyPkcs8, privateKeyId, null); 189 } 190 191 /** 192 * Factory using PKCS#8 for the private key. 193 * 194 * @param clientId Client ID of the service account from the console. May be null. 195 * @param clientEmail Client email address of the service account from the console. 196 * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. 197 * @param privateKeyId Private key identifier for the service account. May be null. 198 * @param defaultAudience Audience to use if not provided by transport. May be null. 199 * @return New ServiceAccountJwtAcceessCredentials created from a private key. 200 * @throws IOException if the credential cannot be created from the private key. 201 */ fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, URI defaultAudience)202 public static ServiceAccountJwtAccessCredentials fromPkcs8( 203 String clientId, 204 String clientEmail, 205 String privateKeyPkcs8, 206 String privateKeyId, 207 URI defaultAudience) 208 throws IOException { 209 return ServiceAccountJwtAccessCredentials.fromPkcs8( 210 clientId, clientEmail, privateKeyPkcs8, privateKeyId, defaultAudience, null); 211 } 212 fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, URI defaultAudience, String quotaProjectId)213 static ServiceAccountJwtAccessCredentials fromPkcs8( 214 String clientId, 215 String clientEmail, 216 String privateKeyPkcs8, 217 String privateKeyId, 218 URI defaultAudience, 219 String quotaProjectId) 220 throws IOException { 221 PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8); 222 return new ServiceAccountJwtAccessCredentials( 223 clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId); 224 } 225 226 /** 227 * Returns credentials defined by a Service Account key file in JSON format from the Google 228 * Developers Console. 229 * 230 * @param credentialsStream the stream with the credential definition. 231 * @return the credential defined by the credentialsStream. 232 * @throws IOException if the credential cannot be created from the stream. 233 */ fromStream(InputStream credentialsStream)234 public static ServiceAccountJwtAccessCredentials fromStream(InputStream credentialsStream) 235 throws IOException { 236 return fromStream(credentialsStream, null); 237 } 238 239 /** 240 * Returns credentials defined by a Service Account key file in JSON format from the Google 241 * Developers Console. 242 * 243 * @param credentialsStream the stream with the credential definition. 244 * @param defaultAudience Audience to use if not provided by transport. May be null. 245 * @return the credential defined by the credentialsStream. 246 * @throws IOException if the credential cannot be created from the stream. 247 */ fromStream( InputStream credentialsStream, URI defaultAudience)248 public static ServiceAccountJwtAccessCredentials fromStream( 249 InputStream credentialsStream, URI defaultAudience) throws IOException { 250 Preconditions.checkNotNull(credentialsStream); 251 252 JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; 253 JsonObjectParser parser = new JsonObjectParser(jsonFactory); 254 GenericJson fileContents = 255 parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class); 256 257 String fileType = (String) fileContents.get("type"); 258 if (fileType == null) { 259 throw new IOException("Error reading credentials from stream, 'type' field not specified."); 260 } 261 if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) { 262 return fromJson(fileContents, defaultAudience); 263 } 264 throw new IOException( 265 String.format( 266 "Error reading credentials from stream, 'type' value '%s' not recognized." 267 + " Expecting '%s'.", 268 fileType, SERVICE_ACCOUNT_FILE_TYPE)); 269 } 270 createCache()271 private LoadingCache<JwtClaims, JwtCredentials> createCache() { 272 return CacheBuilder.newBuilder() 273 .maximumSize(100) 274 .expireAfterWrite(LIFE_SPAN_SECS - CLOCK_SKEW, TimeUnit.SECONDS) 275 .ticker( 276 new Ticker() { 277 @Override 278 public long read() { 279 return TimeUnit.MILLISECONDS.toNanos(clock.currentTimeMillis()); 280 } 281 }) 282 .build( 283 new CacheLoader<JwtClaims, JwtCredentials>() { 284 @Override 285 public JwtCredentials load(JwtClaims claims) throws Exception { 286 return JwtCredentials.newBuilder() 287 .setPrivateKey(privateKey) 288 .setPrivateKeyId(privateKeyId) 289 .setJwtClaims(claims) 290 .setLifeSpanSeconds(LIFE_SPAN_SECS) 291 .setClock(clock) 292 .build(); 293 } 294 }); 295 } 296 297 /** 298 * Returns a new JwtCredentials instance with modified claims. 299 * 300 * @param newClaims new claims. Any unspecified claim fields will default to the the current 301 * values. 302 * @return new credentials 303 */ 304 @Override 305 public JwtCredentials jwtWithClaims(JwtClaims newClaims) { 306 JwtClaims.Builder claimsBuilder = 307 JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail); 308 if (defaultAudience != null) { 309 claimsBuilder.setAudience(defaultAudience.toString()); 310 } 311 return JwtCredentials.newBuilder() 312 .setPrivateKey(privateKey) 313 .setPrivateKeyId(privateKeyId) 314 .setJwtClaims(claimsBuilder.build().merge(newClaims)) 315 .setLifeSpanSeconds(LIFE_SPAN_SECS) 316 .setClock(clock) 317 .build(); 318 } 319 320 @Override 321 public String getAuthenticationType() { 322 return "JWTAccess"; 323 } 324 325 @Override 326 public boolean hasRequestMetadata() { 327 return true; 328 } 329 330 @Override 331 public boolean hasRequestMetadataOnly() { 332 return true; 333 } 334 335 @Override 336 public void getRequestMetadata( 337 final URI uri, Executor executor, final RequestMetadataCallback callback) { 338 // It doesn't use network. Only some CPU work on par with TLS handshake. So it's preferrable 339 // to do it in the current thread, which is likely to be the network thread. 340 blockingGetToCallback(uri, callback); 341 } 342 343 /** Provide the request metadata by putting an access JWT directly in the metadata. */ 344 @Override 345 public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException { 346 if (uri == null) { 347 if (defaultAudience != null) { 348 uri = defaultAudience; 349 } else { 350 throw new IOException( 351 "JwtAccess requires Audience uri to be passed in or the " 352 + "defaultAudience to be specified"); 353 } 354 } 355 356 try { 357 JwtClaims defaultClaims = 358 JwtClaims.newBuilder() 359 .setAudience(uri.toString()) 360 .setIssuer(clientEmail) 361 .setSubject(clientEmail) 362 .build(); 363 JwtCredentials credentials = credentialsCache.get(defaultClaims); 364 Map<String, List<String>> requestMetadata = credentials.getRequestMetadata(uri); 365 return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata); 366 } catch (ExecutionException e) { 367 Throwables.propagateIfPossible(e.getCause(), IOException.class); 368 // Should never happen 369 throw new IllegalStateException( 370 "generateJwtAccess threw an unexpected checked exception", e.getCause()); 371 372 } catch (UncheckedExecutionException e) { 373 Throwables.throwIfUnchecked(e); 374 // Should never happen 375 throw new IllegalStateException( 376 "generateJwtAccess threw an unchecked exception that couldn't be rethrown", e); 377 } 378 } 379 380 /** Discard any cached data */ 381 @Override 382 public void refresh() { 383 credentialsCache.invalidateAll(); 384 } 385 386 public final String getClientId() { 387 return clientId; 388 } 389 390 public final String getClientEmail() { 391 return clientEmail; 392 } 393 394 public final PrivateKey getPrivateKey() { 395 return privateKey; 396 } 397 398 public final String getPrivateKeyId() { 399 return privateKeyId; 400 } 401 402 @Override 403 public String getAccount() { 404 return getClientEmail(); 405 } 406 407 @Override 408 public byte[] sign(byte[] toSign) { 409 try { 410 Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM); 411 signer.initSign(getPrivateKey()); 412 signer.update(toSign); 413 return signer.sign(); 414 } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { 415 throw new ServiceAccountSigner.SigningException("Failed to sign the provided bytes", ex); 416 } 417 } 418 419 @Override 420 public int hashCode() { 421 return Objects.hash( 422 clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId); 423 } 424 425 @Override 426 public String toString() { 427 return MoreObjects.toStringHelper(this) 428 .add("clientId", clientId) 429 .add("clientEmail", clientEmail) 430 .add("privateKeyId", privateKeyId) 431 .add("defaultAudience", defaultAudience) 432 .add("quotaProjectId", quotaProjectId) 433 .toString(); 434 } 435 436 @Override 437 public boolean equals(Object obj) { 438 if (!(obj instanceof ServiceAccountJwtAccessCredentials)) { 439 return false; 440 } 441 ServiceAccountJwtAccessCredentials other = (ServiceAccountJwtAccessCredentials) obj; 442 return Objects.equals(this.clientId, other.clientId) 443 && Objects.equals(this.clientEmail, other.clientEmail) 444 && Objects.equals(this.privateKey, other.privateKey) 445 && Objects.equals(this.privateKeyId, other.privateKeyId) 446 && Objects.equals(this.defaultAudience, other.defaultAudience) 447 && Objects.equals(this.quotaProjectId, other.quotaProjectId); 448 } 449 450 private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { 451 input.defaultReadObject(); 452 clock = Clock.SYSTEM; 453 credentialsCache = createCache(); 454 } 455 456 public static Builder newBuilder() { 457 return new Builder(); 458 } 459 460 public Builder toBuilder() { 461 return new Builder(this); 462 } 463 464 @Override 465 public String getQuotaProjectId() { 466 return quotaProjectId; 467 } 468 469 public static class Builder { 470 471 private String clientId; 472 private String clientEmail; 473 private PrivateKey privateKey; 474 private String privateKeyId; 475 private URI defaultAudience; 476 private String quotaProjectId; 477 478 protected Builder() {} 479 480 protected Builder(ServiceAccountJwtAccessCredentials credentials) { 481 this.clientId = credentials.clientId; 482 this.clientEmail = credentials.clientEmail; 483 this.privateKey = credentials.privateKey; 484 this.privateKeyId = credentials.privateKeyId; 485 this.defaultAudience = credentials.defaultAudience; 486 this.quotaProjectId = credentials.quotaProjectId; 487 } 488 489 @CanIgnoreReturnValue 490 public Builder setClientId(String clientId) { 491 this.clientId = clientId; 492 return this; 493 } 494 495 @CanIgnoreReturnValue 496 public Builder setClientEmail(String clientEmail) { 497 this.clientEmail = clientEmail; 498 return this; 499 } 500 501 @CanIgnoreReturnValue 502 public Builder setPrivateKey(PrivateKey privateKey) { 503 this.privateKey = privateKey; 504 return this; 505 } 506 507 @CanIgnoreReturnValue 508 public Builder setPrivateKeyId(String privateKeyId) { 509 this.privateKeyId = privateKeyId; 510 return this; 511 } 512 513 @CanIgnoreReturnValue 514 public Builder setDefaultAudience(URI defaultAudience) { 515 this.defaultAudience = defaultAudience; 516 return this; 517 } 518 519 @CanIgnoreReturnValue 520 public Builder setQuotaProjectId(String quotaProjectId) { 521 this.quotaProjectId = quotaProjectId; 522 return this; 523 } 524 525 public String getClientId() { 526 return clientId; 527 } 528 529 public String getClientEmail() { 530 return clientEmail; 531 } 532 533 public PrivateKey getPrivateKey() { 534 return privateKey; 535 } 536 537 public String getPrivateKeyId() { 538 return privateKeyId; 539 } 540 541 public URI getDefaultAudience() { 542 return defaultAudience; 543 } 544 545 public String getQuotaProjectId() { 546 return quotaProjectId; 547 } 548 549 public ServiceAccountJwtAccessCredentials build() { 550 return new ServiceAccountJwtAccessCredentials( 551 clientId, clientEmail, privateKey, privateKeyId, defaultAudience, quotaProjectId); 552 } 553 } 554 } 555