1 /* 2 * Copyright 2019, 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 com.google.api.client.http.GenericUrl; 35 import com.google.api.client.http.HttpRequest; 36 import com.google.api.client.http.HttpResponse; 37 import com.google.api.client.http.HttpStatusCodes; 38 import com.google.api.client.http.HttpTransport; 39 import com.google.api.client.http.json.JsonHttpContent; 40 import com.google.api.client.json.GenericJson; 41 import com.google.api.client.json.JsonObjectParser; 42 import com.google.api.client.util.GenericData; 43 import com.google.auth.Credentials; 44 import com.google.auth.ServiceAccountSigner; 45 import com.google.auth.http.HttpCredentialsAdapter; 46 import com.google.common.io.BaseEncoding; 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.util.Map; 50 51 /** 52 * This internal class provides shared utilities for interacting with the IAM API for common 53 * features like signing. 54 */ 55 class IamUtils { 56 private static final String SIGN_BLOB_URL_FORMAT = 57 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob"; 58 private static final String ID_TOKEN_URL_FORMAT = 59 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken"; 60 private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. "; 61 private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. "; 62 63 /** 64 * Returns a signature for the provided bytes. 65 * 66 * @param serviceAccountEmail the email address for the service account used for signing 67 * @param credentials credentials required for making the IAM call 68 * @param transport transport used for building the HTTP request 69 * @param toSign bytes to sign 70 * @param additionalFields additional fields to send in the IAM call 71 * @return signed bytes 72 * @throws ServiceAccountSigner.SigningException if signing fails 73 */ sign( String serviceAccountEmail, Credentials credentials, HttpTransport transport, byte[] toSign, Map<String, ?> additionalFields)74 static byte[] sign( 75 String serviceAccountEmail, 76 Credentials credentials, 77 HttpTransport transport, 78 byte[] toSign, 79 Map<String, ?> additionalFields) { 80 BaseEncoding base64 = BaseEncoding.base64(); 81 String signature; 82 try { 83 signature = 84 getSignature( 85 serviceAccountEmail, credentials, transport, base64.encode(toSign), additionalFields); 86 } catch (IOException ex) { 87 throw new ServiceAccountSigner.SigningException("Failed to sign the provided bytes", ex); 88 } 89 return base64.decode(signature); 90 } 91 getSignature( String serviceAccountEmail, Credentials credentials, HttpTransport transport, String bytes, Map<String, ?> additionalFields)92 private static String getSignature( 93 String serviceAccountEmail, 94 Credentials credentials, 95 HttpTransport transport, 96 String bytes, 97 Map<String, ?> additionalFields) 98 throws IOException { 99 String signBlobUrl = String.format(SIGN_BLOB_URL_FORMAT, serviceAccountEmail); 100 GenericUrl genericUrl = new GenericUrl(signBlobUrl); 101 102 GenericData signRequest = new GenericData(); 103 signRequest.set("payload", bytes); 104 for (Map.Entry<String, ?> entry : additionalFields.entrySet()) { 105 signRequest.set(entry.getKey(), entry.getValue()); 106 } 107 JsonHttpContent signContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, signRequest); 108 109 HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(credentials); 110 HttpRequest request = 111 transport.createRequestFactory(adapter).buildPostRequest(genericUrl, signContent); 112 113 JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); 114 request.setParser(parser); 115 request.setThrowExceptionOnExecuteError(false); 116 117 HttpResponse response = request.execute(); 118 int statusCode = response.getStatusCode(); 119 if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) { 120 GenericData responseError = response.parseAs(GenericData.class); 121 Map<String, Object> error = 122 OAuth2Utils.validateMap(responseError, "error", PARSE_ERROR_MESSAGE); 123 String errorMessage = OAuth2Utils.validateString(error, "message", PARSE_ERROR_MESSAGE); 124 throw new IOException( 125 String.format( 126 "Error code %s trying to sign provided bytes: %s", statusCode, errorMessage)); 127 } 128 if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { 129 throw new IOException( 130 String.format( 131 "Unexpected Error code %s trying to sign provided bytes: %s", 132 statusCode, response.parseAsString())); 133 } 134 InputStream content = response.getContent(); 135 if (content == null) { 136 // Throw explicitly here on empty content to avoid NullPointerException from parseAs call. 137 // Mock transports will have success code with empty content by default. 138 throw new IOException("Empty content from sign blob server request."); 139 } 140 141 GenericData responseData = response.parseAs(GenericData.class); 142 return OAuth2Utils.validateString(responseData, "signedBlob", PARSE_ERROR_SIGNATURE); 143 } 144 145 /** 146 * Returns an IdToken issued to the serviceAccount with a specified targetAudience 147 * 148 * @param serviceAccountEmail the email address for the service account to get an ID Token for 149 * @param credentials credentials required for making the IAM call 150 * @param transport transport used for building the HTTP request 151 * @param targetAudience the audience the issued ID token should include 152 * @param additionalFields additional fields to send in the IAM call 153 * @return IdToken issed to the serviceAccount 154 * @throws IOException if the IdToken cannot be issued. 155 * @see 156 * https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateIdToken 157 */ getIdToken( String serviceAccountEmail, Credentials credentials, HttpTransport transport, String targetAudience, boolean includeEmail, Map<String, ?> additionalFields)158 static IdToken getIdToken( 159 String serviceAccountEmail, 160 Credentials credentials, 161 HttpTransport transport, 162 String targetAudience, 163 boolean includeEmail, 164 Map<String, ?> additionalFields) 165 throws IOException { 166 167 String idTokenUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail); 168 GenericUrl genericUrl = new GenericUrl(idTokenUrl); 169 170 GenericData idTokenRequest = new GenericData(); 171 idTokenRequest.set("audience", targetAudience); 172 idTokenRequest.set("includeEmail", includeEmail); 173 for (Map.Entry<String, ?> entry : additionalFields.entrySet()) { 174 idTokenRequest.set(entry.getKey(), entry.getValue()); 175 } 176 JsonHttpContent idTokenContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, idTokenRequest); 177 178 HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(credentials); 179 HttpRequest request = 180 transport.createRequestFactory(adapter).buildPostRequest(genericUrl, idTokenContent); 181 182 JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); 183 request.setParser(parser); 184 request.setThrowExceptionOnExecuteError(false); 185 186 HttpResponse response = request.execute(); 187 int statusCode = response.getStatusCode(); 188 if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) { 189 GenericData responseError = response.parseAs(GenericData.class); 190 Map<String, Object> error = 191 OAuth2Utils.validateMap(responseError, "error", PARSE_ERROR_MESSAGE); 192 String errorMessage = OAuth2Utils.validateString(error, "message", PARSE_ERROR_MESSAGE); 193 throw new IOException( 194 String.format("Error code %s trying to getIDToken: %s", statusCode, errorMessage)); 195 } 196 if (statusCode != HttpStatusCodes.STATUS_CODE_OK) { 197 throw new IOException( 198 String.format( 199 "Unexpected Error code %s trying to getIDToken: %s", 200 statusCode, response.parseAsString())); 201 } 202 InputStream content = response.getContent(); 203 if (content == null) { 204 // Throw explicitly here on empty content to avoid NullPointerException from 205 // parseAs call. 206 // Mock transports will have success code with empty content by default. 207 throw new IOException("Empty content from generateIDToken server request."); 208 } 209 210 GenericJson responseData = response.parseAs(GenericJson.class); 211 String rawToken = OAuth2Utils.validateString(responseData, "token", PARSE_ERROR_MESSAGE); 212 return IdToken.create(rawToken); 213 } 214 } 215