-
Notifications
You must be signed in to change notification settings - Fork 227
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: adds support for 3PI credentials #464
Merged
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7c40395
feat: adds base external account credentials class and support for fi…
lsirac a15f593
fix: javadoc changes
lsirac 4f59b30
fix: address review comments
lsirac 289d603
fix: nits
lsirac 0a405dd
fix: fix broken test
lsirac cfc042a
fix: format
lsirac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
449 changes: 449 additions & 0 deletions
449
oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
Large diffs are not rendered by default.
Oops, something went wrong.
244 changes: 244 additions & 0 deletions
244
oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
/* | ||
* Copyright 2020 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 Inc. 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.http.GenericUrl; | ||
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.JsonObjectParser; | ||
import com.google.auth.http.HttpTransportFactory; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Paths; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Url-sourced and file-sourced external account credentials. | ||
* | ||
* <p>By default, attempts to exchange the 3PI 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 { | ||
|
||
enum IdentityPoolCredentialSourceType { | ||
FILE, | ||
URL | ||
} | ||
|
||
private String credentialLocation; | ||
private IdentityPoolCredentialSourceType credentialSourceType; | ||
|
||
@Nullable private Map<String, String> headers; | ||
|
||
public IdentityPoolCredentialSource(Map<String, Object> credentialSourceMap) { | ||
super(credentialSourceMap); | ||
|
||
if (credentialSourceMap.containsKey("file")) { | ||
credentialLocation = (String) credentialSourceMap.get("file"); | ||
credentialSourceType = IdentityPoolCredentialSourceType.FILE; | ||
} else { | ||
credentialLocation = (String) credentialSourceMap.get("url"); | ||
credentialSourceType = IdentityPoolCredentialSourceType.URL; | ||
} | ||
|
||
Map<String, String> headersMap = (Map<String, String>) credentialSourceMap.get("headers"); | ||
if (headersMap != null && !headersMap.isEmpty()) { | ||
headers = new HashMap<>(); | ||
headers.putAll(headersMap); | ||
} | ||
} | ||
|
||
private boolean hasHeaders() { | ||
return headers != null && !headers.isEmpty(); | ||
} | ||
} | ||
|
||
/** | ||
* Internal constructor. See {@link | ||
* ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, String, | ||
* String, String, CredentialSource, String, String, String, String, Collection)} | ||
*/ | ||
IdentityPoolCredentials( | ||
HttpTransportFactory transportFactory, | ||
String audience, | ||
String subjectTokenType, | ||
String tokenUrl, | ||
String tokenInfoUrl, | ||
IdentityPoolCredentialSource credentialSource, | ||
@Nullable String serviceAccountImpersonationUrl, | ||
@Nullable String quotaProjectId, | ||
@Nullable String clientId, | ||
@Nullable String clientSecret, | ||
@Nullable Collection<String> scopes) { | ||
super( | ||
transportFactory, | ||
audience, | ||
subjectTokenType, | ||
tokenUrl, | ||
tokenInfoUrl, | ||
credentialSource, | ||
serviceAccountImpersonationUrl, | ||
quotaProjectId, | ||
clientId, | ||
clientSecret, | ||
scopes); | ||
} | ||
|
||
@Override | ||
public AccessToken refreshAccessToken() throws IOException { | ||
String credential = retrieveSubjectToken(); | ||
chingor13 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
StsTokenExchangeRequest.Builder stsTokenExchangeRequest = | ||
StsTokenExchangeRequest.newBuilder(credential, subjectTokenType).setAudience(audience); | ||
|
||
if (scopes != null && !scopes.isEmpty()) { | ||
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); | ||
} | ||
|
||
AccessToken accessToken = exchange3PICredentialForAccessToken(stsTokenExchangeRequest.build()); | ||
return attemptServiceAccountImpersonation(accessToken); | ||
} | ||
|
||
@Override | ||
public String retrieveSubjectToken() throws IOException { | ||
IdentityPoolCredentialSource identityPoolCredentialSource = | ||
(IdentityPoolCredentialSource) credentialSource; | ||
if (identityPoolCredentialSource.credentialSourceType | ||
== IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) { | ||
return retrieveSubjectTokenFromCredentialFile(); | ||
} | ||
return getSubjectTokenFromMetadataServer(); | ||
} | ||
|
||
private String retrieveSubjectTokenFromCredentialFile() throws IOException { | ||
IdentityPoolCredentialSource identityPoolCredentialSource = | ||
(IdentityPoolCredentialSource) credentialSource; | ||
String credentialFilePath = identityPoolCredentialSource.credentialLocation; | ||
if (!new File(credentialFilePath).isFile()) { | ||
throw new IOException("Invalid credential location. The file does not exist."); | ||
lsirac marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
try { | ||
return new String(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; | ||
|
||
HttpRequest request = | ||
transportFactory | ||
.create() | ||
.createRequestFactory() | ||
.buildGetRequest(new GenericUrl(identityPoolCredentialSource.credentialLocation)); | ||
request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY)); | ||
|
||
if (identityPoolCredentialSource.hasHeaders()) { | ||
HttpHeaders headers = new HttpHeaders(); | ||
headers.putAll(identityPoolCredentialSource.headers); | ||
request.setHeaders(headers); | ||
} | ||
|
||
try { | ||
HttpResponse response = request.execute(); | ||
return response.parseAsString(); | ||
} catch (IOException e) { | ||
throw new IOException( | ||
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e); | ||
} | ||
} | ||
|
||
/** Clones the IdentityPoolCredentials with the specified scopes. */ | ||
@Override | ||
public GoogleCredentials createScoped(Collection<String> newScopes) { | ||
return new IdentityPoolCredentials( | ||
transportFactory, | ||
audience, | ||
subjectTokenType, | ||
tokenUrl, | ||
tokenInfoUrl, | ||
(IdentityPoolCredentialSource) credentialSource, | ||
serviceAccountImpersonationUrl, | ||
quotaProjectId, | ||
clientId, | ||
clientSecret, | ||
newScopes); | ||
} | ||
|
||
public static Builder newBuilder() { | ||
return new Builder(); | ||
} | ||
|
||
public static Builder newBuilder(IdentityPoolCredentials identityPoolCredentials) { | ||
return new Builder(identityPoolCredentials); | ||
} | ||
|
||
public static class Builder extends ExternalAccountCredentials.Builder { | ||
|
||
protected Builder() {} | ||
|
||
protected Builder(ExternalAccountCredentials credentials) { | ||
super(credentials); | ||
} | ||
|
||
@Override | ||
public IdentityPoolCredentials build() { | ||
return new IdentityPoolCredentials( | ||
transportFactory, | ||
audience, | ||
subjectTokenType, | ||
tokenUrl, | ||
tokenInfoUrl, | ||
(IdentityPoolCredentialSource) credentialSource, | ||
serviceAccountImpersonationUrl, | ||
quotaProjectId, | ||
clientId, | ||
clientSecret, | ||
scopes); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,8 @@ | |
import static org.junit.Assert.assertNotNull; | ||
import static org.junit.Assert.assertTrue; | ||
|
||
import com.google.api.client.http.HttpHeaders; | ||
import com.google.api.client.http.HttpResponseException; | ||
import com.google.api.client.json.GenericJson; | ||
import com.google.api.client.json.JsonFactory; | ||
import com.google.api.client.json.jackson2.JacksonFactory; | ||
|
@@ -45,17 +47,24 @@ | |
import java.io.InputStream; | ||
import java.io.UnsupportedEncodingException; | ||
import java.net.URLDecoder; | ||
import java.text.SimpleDateFormat; | ||
import java.util.Calendar; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import javax.annotation.Nullable; | ||
|
||
/** Utilities for test code under com.google.auth. */ | ||
public class TestUtils { | ||
|
||
public static final String UTF_8 = "UTF-8"; | ||
private static final String RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'"; | ||
private static final int VALID_LIFETIME = 300; | ||
|
||
private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); | ||
|
||
public static final String UTF_8 = "UTF-8"; | ||
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. Please see comments on original PR |
||
|
||
public static void assertContainsBearerToken(Map<String, List<String>> metadata, String token) { | ||
assertNotNull(metadata); | ||
assertNotNull(token); | ||
|
@@ -119,5 +128,31 @@ public static String errorJson(String message) throws IOException { | |
return errorResponse.toPrettyString(); | ||
} | ||
|
||
public static HttpResponseException buildHttpResponseException( | ||
String error, @Nullable String errorDescription, @Nullable String errorUri) | ||
throws IOException { | ||
GenericJson json = new GenericJson(); | ||
json.setFactory(JacksonFactory.getDefaultInstance()); | ||
json.set("error", error); | ||
if (errorDescription != null) { | ||
json.set("error_description", errorDescription); | ||
} | ||
if (errorUri != null) { | ||
json.set("error_uri", errorUri); | ||
} | ||
return new HttpResponseException.Builder( | ||
/* statusCode= */ 400, /* statusMessage= */ "statusMessage", new HttpHeaders()) | ||
.setContent(json.toPrettyString()) | ||
.build(); | ||
} | ||
|
||
public static String getDefaultExpireTime() { | ||
Date currentDate = new Date(); | ||
Calendar c = Calendar.getInstance(); | ||
c.setTime(currentDate); | ||
c.add(Calendar.SECOND, VALID_LIFETIME); | ||
return new SimpleDateFormat(RFC3339).format(c.getTime()); | ||
} | ||
|
||
private TestUtils() {} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Should we fail early if this input is invalid and doesn't contain
file
orurl
?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.
Good question, I'm not sure. The input here should be from a file generated by gCloud, and should be valid.