• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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