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
Changes from all commits
eeedf25
9e4edbe
cb92cdd
b6a484b
acb6137
7cdb2d7
3dc6bce
fb05cd1
5e62b2c
42a7376
e7ac146
a8f635c
4445ee7
1df4d94
26aa9bf
9943016
76b1963
ed40e41
5fbaf0f
fec6e61
8dbfd0f
adb01fc
52493ee
2ca47e7
5167ba2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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. "; | ||
|
||
|
@@ -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. | ||
salrashid123 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be any 200 level response? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: (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); | ||
return IdToken.create(rawToken); | ||
} | ||
} |
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 { | ||
chingor13 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
salrashid123 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
salrashid123 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
} |
There was a problem hiding this comment.
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?