Skip to content

Commit

Permalink
feat: support generic token formats in IdentityPoolCredentials (#484)
Browse files Browse the repository at this point in the history
* feat: adds text/json credential source support to IdentityPoolCredentials

* fix: format

* fix: format

* fix: add missing changes to MockExternalAccountCredentialsTransport

* fix: change parseToken to take an InputStream

* fix: charsets

* fix: broken build

* fix: type null check
  • Loading branch information
lsirac authored Oct 15, 2020
1 parent d639a78 commit 4666949
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,19 @@
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.common.annotations.VisibleForTesting;
import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource.CredentialFormatType;
import com.google.common.io.CharStreams;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
Expand All @@ -51,25 +60,31 @@
/**
* 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 {

/**
* The IdentityPool credential source. Dictates the retrieval method of the 3PI credential, which
* can either be through a metadata server or a local file.
*/
@VisibleForTesting
static class IdentityPoolCredentialSource extends CredentialSource {
static class IdentityPoolCredentialSource extends ExternalAccountCredentials.CredentialSource {

enum IdentityPoolCredentialSourceType {
FILE,
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 +96,11 @@ enum IdentityPoolCredentialSourceType {
* <p>If this is URL-based 3p credential, the metadata server URL can be retrieved using the
* `url` key.
*
* <p>The third party 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 +109,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 == null || (!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 +180,7 @@ private boolean hasHeaders() {
clientId,
clientSecret,
scopes);
this.identityPoolCredentialSource = credentialSource;
}

@Override
Expand All @@ -153,8 +199,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 +207,39 @@ 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(new FileInputStream(new File(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(InputStream inputStream) throws IOException {
if (identityPoolCredentialSource.credentialFormatType == CredentialFormatType.TEXT) {
BufferedReader reader =
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
return CharStreams.toString(reader);
}

JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
JsonObjectParser parser = new JsonObjectParser(jsonFactory);
GenericJson fileContents =
parser.parseAndClose(inputStream, StandardCharsets.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 +255,7 @@ private String getSubjectTokenFromMetadataServer() throws IOException {

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

0 comments on commit 4666949

Please sign in to comment.