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.
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
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
@@ -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);
}
}

0 comments on commit a87e3fd

Please sign in to comment.