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: support generic token formats in IdentityPoolCredentials #484

Merged
merged 9 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.auth.http.HttpTransportFactory;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
import com.google.common.annotations.VisibleForTesting;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
Expand All @@ -51,7 +56,7 @@
/**
* Url-sourced and file-sourced external account credentials.
*
* <p>By default, attempts to exchange the 3PI credential for a GCP access token.
* <p>By default, attempts to exchange the third-party credential for a GCP access token.
*/
public class IdentityPoolCredentials extends ExternalAccountCredentials {

Expand All @@ -67,9 +72,16 @@ enum IdentityPoolCredentialSourceType {
URL
}

private String credentialLocation;
enum CredentialFormatType {
TEXT,
JSON
}

private IdentityPoolCredentialSourceType credentialSourceType;
private CredentialFormatType credentialFormatType;
private String credentialLocation;

@Nullable private String subjectTokenFieldName;
@Nullable private Map<String, String> headers;

/**
Expand All @@ -81,6 +93,11 @@ enum IdentityPoolCredentialSourceType {
* <p>If this is URL-based 3p credential, the metadata server URL can be retrieved using the
* `url` key.
*
* <p>The 3P credential can be provided in different formats, such as text or JSON. The format
* can be specified using the `format` header, which will return a map with keys `type` and
* `subject_token_field_name`. If the `type` is json, the `subject_token_field_name` must be
* provided. If no format is provided, we expect the token to be in the raw text format.
*
* <p>Optional headers can be present, and should be keyed by `headers`.
*/
public IdentityPoolCredentialSource(Map<String, Object> credentialSourceMap) {
Expand All @@ -89,23 +106,48 @@ public IdentityPoolCredentialSource(Map<String, Object> credentialSourceMap) {
if (credentialSourceMap.containsKey("file")) {
credentialLocation = (String) credentialSourceMap.get("file");
credentialSourceType = IdentityPoolCredentialSourceType.FILE;
} else {
} else if (credentialSourceMap.containsKey("url")) {
credentialLocation = (String) credentialSourceMap.get("url");
credentialSourceType = IdentityPoolCredentialSourceType.URL;
} else {
throw new IllegalArgumentException(
"Missing credential source file location or URL. At least one must be specified.");
}

Map<String, String> headersMap = (Map<String, String>) credentialSourceMap.get("headers");
if (headersMap != null && !headersMap.isEmpty()) {
headers = new HashMap<>();
headers.putAll(headersMap);
}

// If the format is not provided, we will expect the token to be in the raw text format.
credentialFormatType = CredentialFormatType.TEXT;

Map<String, String> formatMap = (Map<String, String>) credentialSourceMap.get("format");
if (formatMap != null && formatMap.containsKey("type")) {
String type = formatMap.get("type");
if (!type.equals("text") && !type.equals("json")) {
throw new IllegalArgumentException(
String.format("Invalid credential source format type: %s.", type));
}
credentialFormatType =
type.equals("text") ? CredentialFormatType.TEXT : CredentialFormatType.JSON;

if (!formatMap.containsKey("subject_token_field_name")) {
throw new IllegalArgumentException(
"When specifying a JSON credential type, the subject_token_field_name must be set.");
}
subjectTokenFieldName = formatMap.get("subject_token_field_name");
}
}

private boolean hasHeaders() {
return headers != null && !headers.isEmpty();
}
}

private final IdentityPoolCredentialSource identityPoolCredentialSource;

/**
* Internal constructor. See {@link
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String,
Expand Down Expand Up @@ -135,6 +177,7 @@ private boolean hasHeaders() {
clientId,
clientSecret,
scopes);
this.identityPoolCredentialSource = credentialSource;
}

@Override
Expand All @@ -153,8 +196,6 @@ public AccessToken refreshAccessToken() throws IOException {

@Override
public String retrieveSubjectToken() throws IOException {
IdentityPoolCredentialSource identityPoolCredentialSource =
(IdentityPoolCredentialSource) credentialSource;
if (identityPoolCredentialSource.credentialSourceType
== IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) {
return retrieveSubjectTokenFromCredentialFile();
Expand All @@ -163,26 +204,37 @@ public String retrieveSubjectToken() throws IOException {
}

private String retrieveSubjectTokenFromCredentialFile() throws IOException {
IdentityPoolCredentialSource identityPoolCredentialSource =
(IdentityPoolCredentialSource) credentialSource;
String credentialFilePath = identityPoolCredentialSource.credentialLocation;
if (!Files.exists(Paths.get(credentialFilePath), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException(
String.format(
"Invalid credential location. The file at %s does not exist.", credentialFilePath));
}
try {
return new String(Files.readAllBytes(Paths.get(credentialFilePath)));
return parseToken(Files.readAllBytes(Paths.get(credentialFilePath)));
} catch (IOException e) {
throw new IOException(
"Error when attempting to read the subject token from the credential file.", e);
}
}

private String getSubjectTokenFromMetadataServer() throws IOException {
IdentityPoolCredentialSource identityPoolCredentialSource =
(IdentityPoolCredentialSource) credentialSource;
private String parseToken(byte[] token) throws IOException {
if (identityPoolCredentialSource.credentialFormatType == CredentialFormatType.TEXT) {
return new String(token);
}

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
JsonObjectParser parser = new JsonObjectParser(jsonFactory);
GenericJson fileContents =
parser.parseAndClose(new ByteArrayInputStream(token), OAuth2Utils.UTF_8, GenericJson.class);

if (!fileContents.containsKey(identityPoolCredentialSource.subjectTokenFieldName)) {
throw new IOException("Invalid subject token field name. No subject token was found.");
}
return (String) fileContents.get(identityPoolCredentialSource.subjectTokenFieldName);
}

private String getSubjectTokenFromMetadataServer() throws IOException {
HttpRequest request =
transportFactory
.create()
Expand All @@ -198,7 +250,7 @@ private String getSubjectTokenFromMetadataServer() throws IOException {

try {
HttpResponse response = request.execute();
return response.parseAsString();
return parseToken(response.parseAsString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new IOException(
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
Expand Down
Loading