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

fix: add validation for the token URL and service account impersonation URL for Workload Identity Federation #717

Merged
merged 4 commits into from Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 @@ -48,8 +48,11 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -179,6 +182,11 @@ protected ExternalAccountCredentials(
this.environmentProvider =
environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;

validateTokenUrl(tokenUrl);
if (serviceAccountImpersonationUrl != null) {
validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
}

this.impersonatedCredentials = initializeImpersonatedCredentials();
}

Expand Down Expand Up @@ -420,6 +428,60 @@ EnvironmentProvider getEnvironmentProvider() {
return environmentProvider;
}

static void validateTokenUrl(String tokenUrl) {
List<Pattern> patterns = new ArrayList<>();
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^sts\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^sts\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\-sts\\.googleapis\\.com$"));
lsirac marked this conversation as resolved.
Show resolved Hide resolved

if (!isValidUrl(patterns, tokenUrl)) {
throw new IllegalArgumentException("The provided token URL is invalid.");
}
}

static void validateServiceAccountImpersonationInfoUrl(String serviceAccountImpersonationUrl) {
List<Pattern> patterns = new ArrayList<>();
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\.iamcredentials\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^iamcredentials\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^iamcredentials\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$"));
patterns.add(Pattern.compile("^[^\\.\\s\\/\\\\]+\\-iamcredentials\\.googleapis\\.com$"));
lsirac marked this conversation as resolved.
Show resolved Hide resolved

if (!isValidUrl(patterns, serviceAccountImpersonationUrl)) {
throw new IllegalArgumentException(
"The provided service account impersonation URL is invalid.");
}
}

/**
* Returns true if the provided URL's scheme is HTTPS and the host comforms to at least one of the
* provided patterns.
*/
private static boolean isValidUrl(List<Pattern> patterns, String url) {
URI uri;

try {
uri = URI.create(url);
} catch (Exception e) {
return false;
}

// Scheme must be https and host must not be null.
if (uri.getScheme() == null
|| uri.getHost() == null
|| !"https".equals(uri.getScheme().toLowerCase(Locale.US))) {
return false;
}

for (Pattern pattern : patterns) {
Matcher match = pattern.matcher(uri.getHost());
if (match.matches()) {
return true;
}
}
return false;
}

/** Base builder for external account credentials. */
public abstract static class Builder extends GoogleCredentials.Builder {

Expand Down
Expand Up @@ -58,6 +58,8 @@
@RunWith(JUnit4.class)
public class AwsCredentialsTest {

private static final String STS_URL = "https://sts.googleapis.com";

private static final String GET_CALLER_IDENTITY_URL =
"https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";

Expand All @@ -83,7 +85,7 @@ public class AwsCredentialsTest {
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.setTokenUrl("tokenUrl")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
.setCredentialSource(AWS_CREDENTIAL_SOURCE)
.build();
Expand Down Expand Up @@ -495,7 +497,8 @@ public void builder() {
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.setTokenUrl("tokenUrl")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
.setCredentialSource(AWS_CREDENTIAL_SOURCE)
.setTokenInfoUrl("tokenInfoUrl")
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
Expand All @@ -507,7 +510,7 @@ public void builder() {

assertEquals("audience", credentials.getAudience());
assertEquals("subjectTokenType", credentials.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), "tokenUrl");
assertEquals(credentials.getTokenUrl(), STS_URL);
assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl");
assertEquals(
credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL);
Expand Down
Expand Up @@ -44,6 +44,7 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
Expand All @@ -59,7 +60,7 @@
@RunWith(JUnit4.class)
public class ExternalAccountCredentialsTest {

private static final String STS_URL = "https://www.sts.google.com";
private static final String STS_URL = "https://sts.googleapis.com";

static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {

Expand Down Expand Up @@ -176,7 +177,7 @@ public void fromJson_nullJson_throws() {
@Test
public void fromJson_invalidServiceAccountImpersonationUrl_throws() {
GenericJson json = buildJsonIdentityPoolCredential();
json.put("service_account_impersonation_url", "invalid_url");
json.put("service_account_impersonation_url", "https://iamcredentials.googleapis.com");

try {
ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
Expand All @@ -199,6 +200,48 @@ public void fromJson_nullTransport_throws() {
}
}

@Test
public void constructor_invalidTokenUrl() {
try {
new TestExternalAccountCredentials(
transportFactory,
"audience",
"subjectTokenType",
"tokenUrl",
new TestCredentialSource(new HashMap<String, Object>()),
STS_URL,
/* serviceAccountImpersonationUrl= */ null,
"quotaProjectId",
/* clientId= */ null,
/* clientSecret= */ null,
/* scopes= */ null);
fail("Should have failed since an invalid token URL was passed.");
} catch (IllegalArgumentException e) {
assertEquals("The provided token URL is invalid.", e.getMessage());
}
}

@Test
public void constructor_invalidServiceAccountImpersonationUrl() {
try {
new TestExternalAccountCredentials(
transportFactory,
"audience",
"subjectTokenType",
"tokenUrl",
new TestCredentialSource(new HashMap<String, Object>()),
/* tokenInfoUrl= */ null,
"serviceAccountImpersonationUrl",
"quotaProjectId",
/* clientId= */ null,
/* clientSecret= */ null,
/* scopes= */ null);
fail("Should have failed since an invalid token URL was passed.");
} catch (IllegalArgumentException e) {
assertEquals("The provided token URL is invalid.", e.getMessage());
}
}

@Test
public void exchangeExternalCredentialForAccessToken() throws IOException {
ExternalAccountCredentials credential =
Expand Down Expand Up @@ -267,7 +310,7 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException {
transportFactory,
"audience",
"subjectTokenType",
"tokenUrl",
STS_URL,
new TestCredentialSource(new HashMap<String, Object>()),
"tokenInfoUrl",
/* serviceAccountImpersonationUrl= */ null,
Expand All @@ -282,6 +325,104 @@ public void getRequestMetadata_withQuotaProjectId() throws IOException {
assertEquals("quotaProjectId", requestMetadata.get("x-goog-user-project").get(0));
}

@Test
public void validateTokenUrl_validUrls() {
List<String> validUrls =
Arrays.asList(
"https://sts.googleapis.com",
"https://us-east-1.sts.googleapis.com",
"https://US-EAST-1.sts.googleapis.com",
"https://sts.us-east-1.googleapis.com",
"https://sts.US-WEST-1.googleapis.com",
"https://us-east-1-sts.googleapis.com",
"https://US-WEST-1-sts.googleapis.com",
"https://us-west-1-sts.googleapis.com/path?query");

for (String url : validUrls) {
ExternalAccountCredentials.validateTokenUrl(url);
}
}

@Test
public void validateTokenUrl_invalidUrls() {
List<String> invalidUrls =
Arrays.asList(
"https://iamcredentials.googleapis.com",
"sts.googleapis.com",
"https://",
"https://us-eas\\t-1.sts.googleapis.com",
"https:/us-east-1.sts.googleapis.com",
"https://US-WE/ST-1-sts.googleapis.com",
lsirac marked this conversation as resolved.
Show resolved Hide resolved
"https://sts-us-east-1.googleapis.com",
"https://sts-US-WEST-1.googleapis.com",
"testhttps://us-east-1.sts.googleapis.com",
"https://us-east-1.sts.googleapis.comevil.com",
"https://us-east-1.us-east-1.sts.googleapis.com",
"https://us-ea.s.t.sts.googleapis.com",
"https://sts.googleapis.comevil.com",
"hhttps://us-east-1.sts.googleapis.com",
"https://us- -1.sts.googleapis.com",
"https://-sts.googleapis.com");

for (String url : invalidUrls) {
try {
ExternalAccountCredentials.validateTokenUrl(url);
fail("Should have failed since an invalid URL was passed.");
} catch (IllegalArgumentException e) {
assertEquals("The provided token URL is invalid.", e.getMessage());
}
}
}

@Test
public void validateServiceAccountImpersonationUrls_validUrls() {
List<String> validUrls =
Arrays.asList(
"https://iamcredentials.googleapis.com",
lsirac marked this conversation as resolved.
Show resolved Hide resolved
"https://us-east-1.iamcredentials.googleapis.com",
"https://US-EAST-1.iamcredentials.googleapis.com",
"https://iamcredentials.us-east-1.googleapis.com",
"https://iamcredentials.US-WEST-1.googleapis.com",
"https://us-east-1-iamcredentials.googleapis.com",
"https://US-WEST-1-iamcredentials.googleapis.com",
"https://us-west-1-iamcredentials.googleapis.com/path?query");

for (String url : validUrls) {
ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url);
}
}

@Test
public void validateServiceAccountImpersonationUrls_invalidUrls() {
List<String> invalidUrls =
Arrays.asList(
"https://sts.googleapis.com",
"iamcredentials.googleapis.com",
"https://",
"https://us-eas\t-1.iamcredentials.googleapis.com",
"https:/us-east-1.iamcredentials.googleapis.com",
"https://US-WE/ST-1-iamcredentials.googleapis.com",
"https://iamcredentials-us-east-1.googleapis.com",
"https://iamcredentials-US-WEST-1.googleapis.com",
"testhttps://us-east-1.iamcredentials.googleapis.com",
"https://us-east-1.iamcredentials.googleapis.comevil.com",
"https://us-east-1.us-east-1.iamcredentials.googleapis.com",
"https://us-ea.s.t.iamcredentials.googleapis.com",
"https://iamcredentials.googleapis.comevil.com",
"hhttps://us-east-1.iamcredentials.googleapis.com",
"https://us- -1.iamcredentials.googleapis.com",
"https://-sts.googleapis.com");

for (String url : invalidUrls) {
try {
ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url);
fail("Should have failed since an invalid URL was passed.");
} catch (IllegalArgumentException e) {
assertEquals("The provided service account impersonation URL is invalid.", e.getMessage());
}
}
}

private GenericJson buildJsonIdentityPoolCredential() {
GenericJson json = new GenericJson();
json.put("audience", "audience");
Expand Down
Expand Up @@ -59,6 +59,8 @@
@RunWith(JUnit4.class)
public class IdentityPoolCredentialsTest {

private static final String STS_URL = "https://sts.googleapis.com";

private static final Map<String, Object> FILE_CREDENTIAL_SOURCE_MAP =
new HashMap<String, Object>() {
{
Expand All @@ -75,7 +77,7 @@ public class IdentityPoolCredentialsTest {
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.setTokenUrl("tokenUrl")
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
.setCredentialSource(FILE_CREDENTIAL_SOURCE)
.build();
Expand Down Expand Up @@ -422,9 +424,9 @@ public void builder() {
.setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
.setAudience("audience")
.setSubjectTokenType("subjectTokenType")
.setTokenUrl("tokenUrl")
.setCredentialSource(FILE_CREDENTIAL_SOURCE)
.setTokenUrl(STS_URL)
.setTokenInfoUrl("tokenInfoUrl")
.setCredentialSource(FILE_CREDENTIAL_SOURCE)
.setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
.setQuotaProjectId("quotaProjectId")
.setClientId("clientId")
Expand All @@ -434,7 +436,7 @@ public void builder() {

assertEquals("audience", credentials.getAudience());
assertEquals("subjectTokenType", credentials.getSubjectTokenType());
assertEquals(credentials.getTokenUrl(), "tokenUrl");
assertEquals(credentials.getTokenUrl(), STS_URL);
assertEquals(credentials.getTokenInfoUrl(), "tokenInfoUrl");
assertEquals(
credentials.getServiceAccountImpersonationUrl(), SERVICE_ACCOUNT_IMPERSONATION_URL);
Expand Down
Expand Up @@ -68,7 +68,7 @@ public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
private static final String AWS_CREDENTIALS_URL = "https://www.aws-credentials.com";
private static final String AWS_REGION_URL = "https://www.aws-region.com";
private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
private static final String STS_URL = "https://www.sts.google.com";
private static final String STS_URL = "https://sts.googleapis.com";

private static final String SUBJECT_TOKEN = "subjectToken";
private static final String TOKEN_TYPE = "Bearer";
Expand Down