Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add IDTokenCredential support #303

Merged
merged 25 commits into from Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -43,6 +43,9 @@
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.base.MoreObjects;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.auth.oauth2.IdTokenProvider;
import com.google.auth.oauth2.OAuth2Utils;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -52,6 +55,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand All @@ -64,7 +68,7 @@
* <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
* </p>
*/
public class ComputeEngineCredentials extends GoogleCredentials implements ServiceAccountSigner {
public class ComputeEngineCredentials extends GoogleCredentials implements ServiceAccountSigner, IdTokenProvider {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved

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

Expand All @@ -91,6 +95,9 @@ public class ComputeEngineCredentials extends GoogleCredentials implements Servi
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
private static final long serialVersionUID = -4113476462526554235L;

public static final String ID_TOKEN_FORMAT_FULL = "format";
public static final String ID_TOKEN_LICENSES_TRUE = "licenses";

private final String transportFactoryClassName;

private transient HttpTransportFactory transportFactory;
Expand Down Expand Up @@ -138,7 +145,7 @@ public AccessToken refreshAccessToken() throws IOException {
}
InputStream content = response.getContent();
if (content == null) {
// Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
// Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
// Mock transports will have success code with empty content by default.
throw new IOException("Empty content from metadata token server request.");
}
Expand All @@ -151,6 +158,49 @@ public AccessToken refreshAccessToken() throws IOException {
return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
}

/**
* Returns an Google Id Token from the metadata server on ComputeEngine.
*
* @param targetAudience The aud: field the IdToken should include.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
* @param options List of Credential specific options for for the
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
* token. For example, an IDToken for a
* ComputeEngineCredential could have the full formated
* claims returned if
* ComputeCredentials.ID_TOKEN_FORMAT_FULL) is provided as
* a list option. Valid option values are:
* * ComputeCredentials.ID_TOKEN_FORMAT_FULL<br>
* * ComputeCredentials.ID_TOKEN_LICENSES_TRUE<br>
* If no options are set, the default
* are "&amp;format=standard&amp;licenses=false"
* @throws IdTokenProvider.IdTokenProviderException if the attempt to get an
* IdToken failed
* @return IdToken object which includes the raw id_token, JsonWebSignature.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
*/
@Override
public IdToken idTokenWithAudience(String targetAudience, List<String> options) {
String optionalParams = "";
if (options != null) {
if (options.contains(ID_TOKEN_FORMAT_FULL))
optionalParams = "&format=full";
if (options.contains(ID_TOKEN_LICENSES_TRUE))
optionalParams = optionalParams + "&licenses=TRUE";
}
String documentURL = getIdentityDocumentUrl() + "?audience=" + targetAudience + optionalParams;
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
HttpResponse response = null;
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
try {
response = getMetadataResponse(documentURL);
InputStream content = response.getContent();
if (content == null) {
throw new IOException("Empty content from metadata token server request.");
}
String rawToken = response.parseAsString();
JsonWebSignature jws = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, rawToken);
return new IdToken(rawToken, jws);
} catch (IOException ex) {
throw new IdTokenProvider.IdTokenProviderException("Unable to get identity document ", ex);
}
}

private HttpResponse getMetadataResponse(String url) throws IOException {
GenericUrl genericUrl = new GenericUrl(url);
HttpRequest request = transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
Expand Down Expand Up @@ -229,6 +279,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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not for this PR, but this looks really wonky. Note to myself: can this possibly be correct?

Expand Down Expand Up @@ -339,4 +394,4 @@ public ComputeEngineCredentials build() {
return new ComputeEngineCredentials(transportFactory);
}
}
}
}
99 changes: 88 additions & 11 deletions oauth2_http/java/com/google/auth/oauth2/IamUtils.java
Expand Up @@ -31,30 +31,33 @@

package com.google.auth.oauth2;

import com.google.api.client.http.HttpTransport;
import com.google.auth.Credentials;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
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.json.webtoken.JsonWebSignature;
import com.google.api.client.util.GenericData;
import com.google.auth.Credentials;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.common.io.BaseEncoding;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
* This internal class provides shared utilities for interacting with the IAM API for common
* features like signing.
*/
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 @@ -96,7 +99,7 @@ private static String getSignature(String serviceAccountEmail, Credentials crede

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

JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
request.setParser(parser);
Expand All @@ -109,11 +112,11 @@ private static String getSignature(String serviceAccountEmail, Credentials crede
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 sign provided bytes: %s",
statusCode, errorMessage));
statusCode, errorMessage));
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
}
if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
throw new IOException(String.format("Unexpected Error code %s trying to sign provided bytes: %s", statusCode,
response.parseAsString()));
response.parseAsString()));
}
InputStream content = response.getContent();
if (content == null) {
Expand All @@ -125,4 +128,78 @@ private static String getSignature(String serviceAccountEmail, Credentials crede
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
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
* 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 New IdToken issed to the serviceAccount.
* @throws IOException if the IdToken cannot be issued.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
*/

static IdToken getIdToken(String serviceAccountEmail, Credentials credentials, HttpTransport transport,
String targetAudience, boolean includeEmail, Map<String, ?> additionalFields) {
IdToken token;
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
try {
token = getOIDCToken(serviceAccountEmail, credentials, transport, targetAudience, includeEmail, additionalFields);
} catch (IOException ex) {
throw new IdTokenProvider.IdTokenProviderException("Unexpected Error while getting ID Token: " + ex.getMessage(),
ex);
}
return token;
}

private static IdToken getOIDCToken(String serviceAccountEmail, Credentials credentials, HttpTransport transport,
String targetAudience, boolean includeEmail, Map<String, ?> additionalFields)
throws IOException, IdTokenProvider.IdTokenProviderException {
String signBlobUrl = String.format(ID_TOKEN_URL_FORMAT, serviceAccountEmail);
GenericUrl genericUrl = new GenericUrl(signBlobUrl);

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

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

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be any 200 level response?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not absolutely sure but i've never seen responses range 2xx from this api layer:
ref: https://cloud.google.com/apis/design/errors#handling_errors
but storage mentions a bit of that here: https://cloud.google.com/storage/docs/json_api/v1/status-codes

(the google apis endpoint iamcredentials uses is older one (similar to drive api, calendar api, etc)

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);
JsonWebSignature jws = JsonWebSignature.parse(OAuth2Utils.JSON_FACTORY, rawToken);
return new IdToken(rawToken, jws);
}

}
100 changes: 100 additions & 0 deletions oauth2_http/java/com/google/auth/oauth2/IdToken.java
@@ -0,0 +1,100 @@
/*
* Copyright 2015, Google Inc. All rights reserved.
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
*
* 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 Inc. nor the names of its
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
* 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 java.io.Serializable;
import java.util.Date;
import java.util.Objects;

import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.common.base.MoreObjects;

/**
* Represents a temporary IdToken and its JSONWebSingature object.
*/
public class IdToken extends AccessToken implements Serializable {

private static final long serialVersionUID = -8514239465808977353L;

private final String tokenValue;
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
private final JsonWebSignature jws;
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param tokenValue String representation of the Id token.
* @param jws JsonWebSignature as object
* @param audience List of the Audiences the idToken was issued for.
*/
public IdToken(String tokenValue, JsonWebSignature jws) {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
super(tokenValue, new Date(jws.getPayload().getExpirationTimeSeconds()));
this.tokenValue = tokenValue;
this.jws = jws;
}

/**
* String representation of the Id Token.
*
* @return The raw access token string value.
*/
public String getTokenValue() {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
return tokenValue;
}

/**
* The JsonWebSignature as object
*
* @return List of audiences.
*/
public JsonWebSignature getJsonWebSignature() {
return jws;
}

@Override
public int hashCode() {
return Objects.hash(tokenValue, jws);
}

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

@Override
public boolean equals(Object obj) {
if (!(obj instanceof AccessToken)) {
salrashid123 marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
IdToken other = (IdToken) obj;
return Objects.equals(this.tokenValue, other.tokenValue)
&& Objects.equals(this.jws, other.jws);
}
}