Skip to content

Commit

Permalink
feat: add IDTokenCredential support (#303)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
salrashid123 authored and chingor13 committed Aug 14, 2019
1 parent 29f58b4 commit a87e3fd
Show file tree
Hide file tree
Showing 15 changed files with 1,389 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.google.api.client.util.GenericData;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -50,6 +51,7 @@
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
Expand All @@ -62,7 +64,8 @@
*
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
*/
public class ComputeEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {
public class ComputeEngineCredentials extends GoogleCredentials
implements ServiceAccountSigner, IdTokenProvider {

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

Expand Down Expand Up @@ -157,6 +160,45 @@ public AccessToken refreshAccessToken() throws IOException {
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}

/**
* Returns a Google ID Token from the metadata server on ComputeEngine
*
* @param targetAudience the aud: field the IdToken should include
* @param options list of Credential specific options for the token. For example, an IDToken for a
* ComputeEngineCredential could have the full formatted claims returned if
* IdTokenProvider.Option.FORMAT_FULL) is provided as a list option. Valid option values are:
* <br>
* IdTokenProvider.Option.FORMAT_FULL<br>
* IdTokenProvider.Option.LICENSES_TRUE<br>
* If no options are set, the defaults are "&amp;format=standard&amp;licenses=false"
* @throws IOException if the attempt to get an IdToken failed
* @return IdToken object which includes the raw id_token, JsonWebSignature
*/
@Beta
@Override
public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)
throws IOException {
GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl());
if (options != null) {
if (options.contains(IdTokenProvider.Option.FORMAT_FULL)) {
documentUrl.set("format", "full");
}
if (options.contains(IdTokenProvider.Option.LICENSES_TRUE)) {
// license will only get returned if format is also full
documentUrl.set("format", "full");
documentUrl.set("license", "TRUE");
}
}
documentUrl.set("audience", targetAudience);
HttpResponse response = getMetadataResponse(documentUrl.toString());
InputStream content = response.getContent();
if (content == null) {
throw new IOException("Empty content from metadata token server request.");
}
String rawToken = response.parseAsString();
return IdToken.create(rawToken);
}

private HttpResponse getMetadataResponse(String url) throws IOException {
GenericUrl genericUrl = new GenericUrl(url);
HttpRequest request =
Expand Down Expand Up @@ -243,6 +285,11 @@ public static String getServiceAccountsUrl() {
+ "/computeMetadata/v1/instance/service-accounts/?recursive=true";
}

public static String getIdentityDocumentUrl() {
return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
+ "/computeMetadata/v1/instance/service-accounts/default/identity";
}

@Override
public int hashCode() {
return Objects.hash(transportFactoryClassName);
Expand Down
73 changes: 73 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/IamUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.auth.Credentials;
Expand All @@ -54,6 +55,8 @@
class IamUtils {
private static final String SIGN_BLOB_URL_FORMAT =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";
private static final String ID_TOKEN_URL_FORMAT =
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken";
private static final String PARSE_ERROR_MESSAGE = "Error parsing error message response. ";
private static final String PARSE_ERROR_SIGNATURE = "Error parsing signature response. ";

Expand Down Expand Up @@ -138,4 +141,74 @@ private static String getSignature(
GenericData responseData = response.parseAs(GenericData.class);
return OAuth2Utils.validateString(responseData, "signedBlob", PARSE_ERROR_SIGNATURE);
}

/**
* Returns an IdToken issued to the serviceAccount with a specified targetAudience
*
* @param serviceAccountEmail the email address for the service account to get an ID Token for
* @param credentials credentials required for making the IAM call
* @param transport transport used for building the HTTP request
* @param targetAudience the audience the issued ID token should include
* @param additionalFields additional fields to send in the IAM call
* @return IdToken issed to the serviceAccount
* @throws IOException if the IdToken cannot be issued.
* @see
* https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/generateIdToken
*/
static IdToken getIdToken(
String serviceAccountEmail,
Credentials credentials,
HttpTransport transport,
String targetAudience,
boolean includeEmail,
Map<String, ?> additionalFields)
throws IOException {

String idTokenUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail);
GenericUrl genericUrl = new GenericUrl(idTokenUrl);

GenericData idTokenRequest = new GenericData();
idTokenRequest.set("audience", targetAudience);
idTokenRequest.set("includeEmail", includeEmail);
for (Map.Entry<String, ?> entry : additionalFields.entrySet()) {
idTokenRequest.set(entry.getKey(), entry.getValue());
}
JsonHttpContent idTokenContent = new JsonHttpContent(OAuth2Utils.JSON_FACTORY, idTokenRequest);

HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(credentials);
HttpRequest request =
transport.createRequestFactory(adapter).buildPostRequest(genericUrl, idTokenContent);

JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
request.setParser(parser);
request.setThrowExceptionOnExecuteError(false);

HttpResponse response = request.execute();
int statusCode = response.getStatusCode();
if (statusCode >= 400 && statusCode < HttpStatusCodes.STATUS_CODE_SERVER_ERROR) {
GenericData responseError = response.parseAs(GenericData.class);
Map<String, Object> error =
OAuth2Utils.validateMap(responseError, "error", PARSE_ERROR_MESSAGE);
String errorMessage = OAuth2Utils.validateString(error, "message", PARSE_ERROR_MESSAGE);
throw new IOException(
String.format("Error code %s trying to getIDToken: %s", statusCode, errorMessage));
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
throw new IOException(
String.format(
"Unexpected Error code %s trying to getIDToken: %s",
statusCode, response.parseAsString()));
}
InputStream content = response.getContent();
if (content == null) {
// Throw explicitly here on empty content to avoid NullPointerException from
// parseAs call.
// Mock transports will have success code with empty content by default.
throw new IOException("Empty content from generateIDToken server request.");
}

GenericJson responseData = response.parseAs(GenericJson.class);
String rawToken = OAuth2Utils.validateString(responseData, "token", PARSE_ERROR_MESSAGE);
return IdToken.create(rawToken);
}
}
125 changes: 125 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/IdToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2019, Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.auth.oauth2;

import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.common.annotations.Beta;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;

/** Represents a temporary IdToken and its JsonWebSignature object */
@Beta
public class IdToken extends AccessToken implements Serializable {

private static final long serialVersionUID = -8514239465808977353L;

private transient JsonWebSignature jsonWebSignature;

/**
* @param tokenValue String representation of the ID token.
* @param jsonWebSignature JsonWebSignature as object
*/
private IdToken(String tokenValue, JsonWebSignature jsonWebSignature) {
super(tokenValue, new Date(jsonWebSignature.getPayload().getExpirationTimeSeconds() * 1000));
this.jsonWebSignature = jsonWebSignature;
}

/**
* Creates an IdToken given the encoded Json Web Signature.
*
* @param tokenValue String representation of the ID token.
* @return returns com.google.auth.oauth2.IdToken
*/
public static IdToken create(String tokenValue) throws IOException {
return create(tokenValue, OAuth2Utils.JSON_FACTORY);
}

/**
* Creates an IdToken given the encoded Json Web Signature and JSON Factory
*
* @param jsonFactory JsonFactory to use for parsing the provided token.
* @param tokenValue String representation of the ID token.
* @return returns com.google.auth.oauth2.IdToken
*/
public static IdToken create(String tokenValue, JsonFactory jsonFactory) throws IOException {
return new IdToken(tokenValue, JsonWebSignature.parse(jsonFactory, tokenValue));
}

/**
* The JsonWebSignature as object
*
* @return returns com.google.api.client.json.webtoken.JsonWebSignature
*/
public JsonWebSignature getJsonWebSignature() {
return jsonWebSignature;
}

@Override
public int hashCode() {
return Objects.hash(
super.getTokenValue(), jsonWebSignature.getHeader(), jsonWebSignature.getPayload());
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("tokenValue", super.getTokenValue())
.add("JsonWebSignature", jsonWebSignature)
.toString();
}

@Override
public boolean equals(Object obj) {
if (!(obj instanceof IdToken)) {
return false;
}
IdToken other = (IdToken) obj;
return Objects.equals(super.getTokenValue(), other.getTokenValue())
&& Objects.equals(this.jsonWebSignature.getHeader(), other.jsonWebSignature.getHeader())
&& Objects.equals(this.jsonWebSignature.getPayload(), other.jsonWebSignature.getPayload());
}

private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeObject(this.getTokenValue());
}

private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
String signature = (String) ois.readObject();
this.jsonWebSignature = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, signature);
}
}
Loading

0 comments on commit a87e3fd

Please sign in to comment.