Skip to content

Commit a87e3fd

Browse files
salrashid123chingor13
authored andcommitted
feat: add IDTokenCredential support (#303)
* Add IDTokenCredential support * Add enum; remove custom Exception; user super * remove unused Exception; use GenericURL * extend OAuth2Credentials * fix expiration time; update retrunCredential time * update formatting/docs * add unittests; format * change to IdTokenProvider; fixes * remove casts; change param name * add beta annotations * use builder() * add const back * update unresolved comments; run formatter * remove condition check for options * html formatting * more formatting * add additional tests * uppercase consts; other fixes * add comments to const * add copyright * copyright copy * add tests * id-ID
1 parent 29f58b4 commit a87e3fd

15 files changed

+1389
-17
lines changed

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

+48-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.api.client.util.GenericData;
4343
import com.google.auth.ServiceAccountSigner;
4444
import com.google.auth.http.HttpTransportFactory;
45+
import com.google.common.annotations.Beta;
4546
import com.google.common.base.MoreObjects;
4647
import java.io.IOException;
4748
import java.io.InputStream;
@@ -50,6 +51,7 @@
5051
import java.net.UnknownHostException;
5152
import java.util.Collections;
5253
import java.util.Date;
54+
import java.util.List;
5355
import java.util.Map;
5456
import java.util.Objects;
5557
import java.util.logging.Level;
@@ -62,7 +64,8 @@
6264
*
6365
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
6466
*/
65-
public class ComputeEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {
67+
public class ComputeEngineCredentials extends GoogleCredentials
68+
implements ServiceAccountSigner, IdTokenProvider {
6669

6770
private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());
6871

@@ -157,6 +160,45 @@ public AccessToken refreshAccessToken() throws IOException {
157160
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
158161
}
159162

163+
/**
164+
* Returns a Google ID Token from the metadata server on ComputeEngine
165+
*
166+
* @param targetAudience the aud: field the IdToken should include
167+
* @param options list of Credential specific options for the token. For example, an IDToken for a
168+
* ComputeEngineCredential could have the full formatted claims returned if
169+
* IdTokenProvider.Option.FORMAT_FULL) is provided as a list option. Valid option values are:
170+
* <br>
171+
* IdTokenProvider.Option.FORMAT_FULL<br>
172+
* IdTokenProvider.Option.LICENSES_TRUE<br>
173+
* If no options are set, the defaults are "&amp;format=standard&amp;licenses=false"
174+
* @throws IOException if the attempt to get an IdToken failed
175+
* @return IdToken object which includes the raw id_token, JsonWebSignature
176+
*/
177+
@Beta
178+
@Override
179+
public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)
180+
throws IOException {
181+
GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl());
182+
if (options != null) {
183+
if (options.contains(IdTokenProvider.Option.FORMAT_FULL)) {
184+
documentUrl.set("format", "full");
185+
}
186+
if (options.contains(IdTokenProvider.Option.LICENSES_TRUE)) {
187+
// license will only get returned if format is also full
188+
documentUrl.set("format", "full");
189+
documentUrl.set("license", "TRUE");
190+
}
191+
}
192+
documentUrl.set("audience", targetAudience);
193+
HttpResponse response = getMetadataResponse(documentUrl.toString());
194+
InputStream content = response.getContent();
195+
if (content == null) {
196+
throw new IOException("Empty content from metadata token server request.");
197+
}
198+
String rawToken = response.parseAsString();
199+
return IdToken.create(rawToken);
200+
}
201+
160202
private HttpResponse getMetadataResponse(String url) throws IOException {
161203
GenericUrl genericUrl = new GenericUrl(url);
162204
HttpRequest request =
@@ -243,6 +285,11 @@ public static String getServiceAccountsUrl() {
243285
+ "/computeMetadata/v1/instance/service-accounts/?recursive=true";
244286
}
245287

288+
public static String getIdentityDocumentUrl() {
289+
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
290+
+ "/computeMetadata/v1/instance/service-accounts/default/identity";
291+
}
292+
246293
@Override
247294
public int hashCode() {
248295
return Objects.hash(transportFactoryClassName);

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

+73
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.google.api.client.http.HttpStatusCodes;
3838
import com.google.api.client.http.HttpTransport;
3939
import com.google.api.client.http.json.JsonHttpContent;
40+
import com.google.api.client.json.GenericJson;
4041
import com.google.api.client.json.JsonObjectParser;
4142
import com.google.api.client.util.GenericData;
4243
import com.google.auth.Credentials;
@@ -54,6 +55,8 @@
5455
class IamUtils {
5556
private static final String SIGN_BLOB_URL_FORMAT =
5657
"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";
5760
private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. ";
5861
private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. ";
5962

@@ -138,4 +141,74 @@ private static String getSignature(
138141
GenericData responseData = response.parseAs(GenericData.class);
139142
return OAuth2Utils.validateString(responseData, "signedBlob", PARSE_ERROR_SIGNATURE);
140143
}
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+
*/
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+
}
141214
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2019, 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 com.google.api.client.json.JsonFactory;
35+
import com.google.api.client.json.webtoken.JsonWebSignature;
36+
import com.google.common.annotations.Beta;
37+
import com.google.common.base.MoreObjects;
38+
import java.io.IOException;
39+
import java.io.ObjectInputStream;
40+
import java.io.ObjectOutputStream;
41+
import java.io.Serializable;
42+
import java.util.Date;
43+
import java.util.Objects;
44+
45+
/** Represents a temporary IdToken and its JsonWebSignature object */
46+
@Beta
47+
public class IdToken extends AccessToken implements Serializable {
48+
49+
private static final long serialVersionUID = -8514239465808977353L;
50+
51+
private transient JsonWebSignature jsonWebSignature;
52+
53+
/**
54+
* @param tokenValue String representation of the ID token.
55+
* @param jsonWebSignature JsonWebSignature as object
56+
*/
57+
private IdToken(String tokenValue, JsonWebSignature jsonWebSignature) {
58+
super(tokenValue, new Date(jsonWebSignature.getPayload().getExpirationTimeSeconds() * 1000));
59+
this.jsonWebSignature = jsonWebSignature;
60+
}
61+
62+
/**
63+
* Creates an IdToken given the encoded Json Web Signature.
64+
*
65+
* @param tokenValue String representation of the ID token.
66+
* @return returns com.google.auth.oauth2.IdToken
67+
*/
68+
public static IdToken create(String tokenValue) throws IOException {
69+
return create(tokenValue, OAuth2Utils.JSON_FACTORY);
70+
}
71+
72+
/**
73+
* Creates an IdToken given the encoded Json Web Signature and JSON Factory
74+
*
75+
* @param jsonFactory JsonFactory to use for parsing the provided token.
76+
* @param tokenValue String representation of the ID token.
77+
* @return returns com.google.auth.oauth2.IdToken
78+
*/
79+
public static IdToken create(String tokenValue, JsonFactory jsonFactory) throws IOException {
80+
return new IdToken(tokenValue, JsonWebSignature.parse(jsonFactory, tokenValue));
81+
}
82+
83+
/**
84+
* The JsonWebSignature as object
85+
*
86+
* @return returns com.google.api.client.json.webtoken.JsonWebSignature
87+
*/
88+
public JsonWebSignature getJsonWebSignature() {
89+
return jsonWebSignature;
90+
}
91+
92+
@Override
93+
public int hashCode() {
94+
return Objects.hash(
95+
super.getTokenValue(), jsonWebSignature.getHeader(), jsonWebSignature.getPayload());
96+
}
97+
98+
@Override
99+
public String toString() {
100+
return MoreObjects.toStringHelper(this)
101+
.add("tokenValue", super.getTokenValue())
102+
.add("JsonWebSignature", jsonWebSignature)
103+
.toString();
104+
}
105+
106+
@Override
107+
public boolean equals(Object obj) {
108+
if (!(obj instanceof IdToken)) {
109+
return false;
110+
}
111+
IdToken other = (IdToken) obj;
112+
return Objects.equals(super.getTokenValue(), other.getTokenValue())
113+
&& Objects.equals(this.jsonWebSignature.getHeader(), other.jsonWebSignature.getHeader())
114+
&& Objects.equals(this.jsonWebSignature.getPayload(), other.jsonWebSignature.getPayload());
115+
}
116+
117+
private void writeObject(ObjectOutputStream oos) throws IOException {
118+
oos.writeObject(this.getTokenValue());
119+
}
120+
121+
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
122+
String signature = (String) ois.readObject();
123+
this.jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, signature);
124+
}
125+
}

0 commit comments

Comments
 (0)