/
IdentityPoolCredentials.java
327 lines (284 loc) · 12.6 KB
/
IdentityPoolCredentials.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
* Copyright 2021 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.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.GenericJson;
import com.google.api.client.json.JsonObjectParser;
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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Url-sourced and file-sourced external account credentials.
*
* <p>By default, attempts to exchange the external credential for a GCP access token.
*/
public class IdentityPoolCredentials extends ExternalAccountCredentials {
/**
* The IdentityPool credential source. Dictates the retrieval method of the external credential,
* which can either be through a metadata server or a local file.
*/
static class IdentityPoolCredentialSource extends ExternalAccountCredentials.CredentialSource {
enum IdentityPoolCredentialSourceType {
FILE,
URL
}
enum CredentialFormatType {
TEXT,
JSON
}
private IdentityPoolCredentialSourceType credentialSourceType;
private CredentialFormatType credentialFormatType;
private String credentialLocation;
@Nullable private String subjectTokenFieldName;
@Nullable private Map<String, String> headers;
/**
* The source of the 3P credential.
*
* <p>If this is a file based 3P credential, the credentials file can be retrieved using the
* `file` key.
*
* <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 returns 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`.
*/
IdentityPoolCredentialSource(Map<String, Object> credentialSourceMap) {
super(credentialSourceMap);
if (credentialSourceMap.containsKey("file") && credentialSourceMap.containsKey("url")) {
throw new IllegalArgumentException(
"Only one credential source type can be set, either file or url.");
}
if (credentialSourceMap.containsKey("file")) {
credentialLocation = (String) credentialSourceMap.get("file");
credentialSourceType = IdentityPoolCredentialSourceType.FILE;
} 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 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 (!"text".equals(type) && !"json".equals(type)) {
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;
// This is used for Workforce Pools. It is passed to STS during token exchange in the
// `options` param and will be embedded in the token by STS.
@Nullable private String workforcePoolUserProject;
/** Internal constructor. See {@link Builder}. */
IdentityPoolCredentials(Builder builder) {
super(
builder.transportFactory,
builder.audience,
builder.subjectTokenType,
builder.tokenUrl,
builder.credentialSource,
builder.tokenInfoUrl,
builder.serviceAccountImpersonationUrl,
builder.quotaProjectId,
builder.clientId,
builder.clientSecret,
builder.scopes,
builder.environmentProvider);
this.identityPoolCredentialSource = (IdentityPoolCredentialSource) builder.credentialSource;
this.workforcePoolUserProject = builder.workforcePoolUserProject;
if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
throw new IllegalArgumentException(
"The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
}
}
@Nullable
public String getWorkforcePoolUserProject() {
return workforcePoolUserProject;
}
@Override
public AccessToken refreshAccessToken() throws IOException {
String credential = retrieveSubjectToken();
StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType())
.setAudience(getAudience());
Collection<String> scopes = getScopes();
if (scopes != null && !scopes.isEmpty()) {
stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes));
}
// If this credential was initialized with a Workforce configuration then the
// workforcePoolUserProject must passed to STS via the the internal options param.
if (isWorkforcePoolConfiguration()) {
GenericJson options = new GenericJson();
options.setFactory(OAuth2Utils.JSON_FACTORY);
options.put("userProject", workforcePoolUserProject);
stsTokenExchangeRequest.setInternalOptions(options.toString());
}
return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build());
}
@Override
public String retrieveSubjectToken() throws IOException {
if (identityPoolCredentialSource.credentialSourceType
== IdentityPoolCredentialSource.IdentityPoolCredentialSourceType.FILE) {
return retrieveSubjectTokenFromCredentialFile();
}
return getSubjectTokenFromMetadataServer();
}
private String retrieveSubjectTokenFromCredentialFile() throws IOException {
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 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 parseToken(InputStream inputStream) throws IOException {
if (identityPoolCredentialSource.credentialFormatType == CredentialFormatType.TEXT) {
BufferedReader reader =
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
return CharStreams.toString(reader);
}
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
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()
.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 parseToken(response.getContent());
} catch (IOException e) {
throw new IOException(
String.format("Error getting subject token from metadata server: %s", e.getMessage()), e);
}
}
/**
* Returns whether or not the current configuration is for Workforce Pools (which enable 3p user
* identities, rather than workloads).
*/
public boolean isWorkforcePoolConfiguration() {
Pattern workforceAudiencePattern =
Pattern.compile("^//iam.googleapis.com/locations/.+/workforcePools/.+/providers/.+$");
return workforcePoolUserProject != null
&& !workforcePoolUserProject.isEmpty()
&& workforceAudiencePattern.matcher(getAudience()).matches();
}
/** Clones the IdentityPoolCredentials with the specified scopes. */
@Override
public IdentityPoolCredentials createScoped(Collection<String> newScopes) {
return new IdentityPoolCredentials(
(IdentityPoolCredentials.Builder) newBuilder(this).setScopes(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 {
@Nullable private String workforcePoolUserProject;
Builder() {}
Builder(IdentityPoolCredentials credentials) {
super(credentials);
setWorkforcePoolUserProject(credentials.getWorkforcePoolUserProject());
}
public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
this.workforcePoolUserProject = workforcePoolUserProject;
return this;
}
@Override
public IdentityPoolCredentials build() {
return new IdentityPoolCredentials(this);
}
}
}