Skip to content

Commit 4a594df

Browse files
committed
fix(auth): close BufferedReader when reading subject token from TEXT file
1 parent cd57169 commit 4a594df

2 files changed

Lines changed: 241 additions & 3 deletions

File tree

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/FileIdentityPoolSubjectTokenSupplier.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,10 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
8484
static String parseToken(InputStream inputStream, IdentityPoolCredentialSource credentialSource)
8585
throws IOException {
8686
if (credentialSource.credentialFormatType == CredentialFormatType.TEXT) {
87-
BufferedReader reader =
88-
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
89-
return CharStreams.toString(reader);
87+
try (BufferedReader reader =
88+
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
89+
return CharStreams.toString(reader);
90+
}
9091
}
9192

9293
JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
import static org.junit.jupiter.api.Assertions.assertEquals;
35+
import static org.junit.jupiter.api.Assertions.assertThrows;
36+
import static org.junit.jupiter.api.Assertions.assertTrue;
37+
38+
import com.google.auth.oauth2.IdentityPoolCredentialSource.CredentialFormatType;
39+
import java.io.ByteArrayInputStream;
40+
import java.io.IOException;
41+
import java.nio.charset.StandardCharsets;
42+
import java.nio.file.Files;
43+
import java.nio.file.Path;
44+
import java.util.HashMap;
45+
import java.util.Map;
46+
import org.junit.jupiter.api.Test;
47+
import org.junit.jupiter.api.io.TempDir;
48+
49+
class FileIdentityPoolSubjectTokenSupplierTest {
50+
51+
@Test
52+
void parseToken_textFormat_closesStream() throws IOException {
53+
String token = "my-subject-token";
54+
byte[] bytes = token.getBytes(StandardCharsets.UTF_8);
55+
TrackingInputStream stream = new TrackingInputStream(bytes);
56+
57+
Map<String, Object> credentialSourceMap = new HashMap<>();
58+
credentialSourceMap.put("file", "/path/to/file");
59+
IdentityPoolCredentialSource credentialSource =
60+
new IdentityPoolCredentialSource(credentialSourceMap);
61+
62+
// Default credential format type is TEXT.
63+
assertEquals(CredentialFormatType.TEXT, credentialSource.credentialFormatType);
64+
65+
String parsedToken = FileIdentityPoolSubjectTokenSupplier.parseToken(stream, credentialSource);
66+
67+
assertEquals(token, parsedToken);
68+
assertTrue(stream.closed, "InputStream should be closed after parsing TEXT format");
69+
}
70+
71+
@Test
72+
void parseToken_jsonFormat_closesStream() throws IOException {
73+
String json = "{\"subjectToken\":\"my-json-token\"}";
74+
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
75+
TrackingInputStream stream = new TrackingInputStream(bytes);
76+
77+
Map<String, Object> credentialSourceMap = new HashMap<>();
78+
credentialSourceMap.put("file", "/path/to/file");
79+
Map<String, String> formatMap = new HashMap<>();
80+
formatMap.put("type", "json");
81+
formatMap.put("subject_token_field_name", "subjectToken");
82+
credentialSourceMap.put("format", formatMap);
83+
84+
IdentityPoolCredentialSource credentialSource =
85+
new IdentityPoolCredentialSource(credentialSourceMap);
86+
87+
assertEquals(CredentialFormatType.JSON, credentialSource.credentialFormatType);
88+
89+
String parsedToken = FileIdentityPoolSubjectTokenSupplier.parseToken(stream, credentialSource);
90+
91+
assertEquals("my-json-token", parsedToken);
92+
assertTrue(stream.closed, "InputStream should be closed after parsing JSON format");
93+
}
94+
95+
@Test
96+
void parseToken_jsonFormat_missingField_closesStream() throws IOException {
97+
String json = "{\"otherField\":\"my-json-token\"}";
98+
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
99+
TrackingInputStream stream = new TrackingInputStream(bytes);
100+
101+
Map<String, Object> credentialSourceMap = new HashMap<>();
102+
credentialSourceMap.put("file", "/path/to/file");
103+
Map<String, String> formatMap = new HashMap<>();
104+
formatMap.put("type", "json");
105+
formatMap.put("subject_token_field_name", "subjectToken");
106+
credentialSourceMap.put("format", formatMap);
107+
108+
IdentityPoolCredentialSource credentialSource =
109+
new IdentityPoolCredentialSource(credentialSourceMap);
110+
111+
assertThrows(
112+
IOException.class,
113+
() -> FileIdentityPoolSubjectTokenSupplier.parseToken(stream, credentialSource));
114+
115+
assertTrue(stream.closed, "InputStream should be closed even if parsing throws an exception");
116+
}
117+
118+
@Test
119+
void parseToken_jsonFormat_malformedJson_closesStream() {
120+
String malformedJson = "{invalid-json";
121+
byte[] bytes = malformedJson.getBytes(StandardCharsets.UTF_8);
122+
TrackingInputStream stream = new TrackingInputStream(bytes);
123+
124+
Map<String, Object> credentialSourceMap = new HashMap<>();
125+
credentialSourceMap.put("file", "/path/to/file");
126+
Map<String, String> formatMap = new HashMap<>();
127+
formatMap.put("type", "json");
128+
formatMap.put("subject_token_field_name", "subjectToken");
129+
credentialSourceMap.put("format", formatMap);
130+
131+
IdentityPoolCredentialSource credentialSource =
132+
new IdentityPoolCredentialSource(credentialSourceMap);
133+
134+
assertThrows(
135+
IOException.class,
136+
() -> FileIdentityPoolSubjectTokenSupplier.parseToken(stream, credentialSource));
137+
138+
assertTrue(
139+
stream.closed, "InputStream should be closed even if parsing throws a parsing exception");
140+
}
141+
142+
@Test
143+
void getSubjectToken_fileDoesNotExist_throwsException() {
144+
Map<String, Object> credentialSourceMap = new HashMap<>();
145+
credentialSourceMap.put("file", "/nonexistent/path/to/file");
146+
IdentityPoolCredentialSource credentialSource =
147+
new IdentityPoolCredentialSource(credentialSourceMap);
148+
149+
FileIdentityPoolSubjectTokenSupplier supplier =
150+
new FileIdentityPoolSubjectTokenSupplier(credentialSource);
151+
152+
IOException exception = assertThrows(IOException.class, () -> supplier.getSubjectToken(null));
153+
assertTrue(exception.getMessage().contains("Invalid credential location"));
154+
}
155+
156+
@Test
157+
void getSubjectToken_textFormat_returnsToken(@TempDir Path tempDir) throws IOException {
158+
Path file = tempDir.resolve("token.txt");
159+
String expectedToken = "text-token-from-file";
160+
Files.write(file, expectedToken.getBytes(StandardCharsets.UTF_8));
161+
162+
Map<String, Object> credentialSourceMap = new HashMap<>();
163+
credentialSourceMap.put("file", file.toAbsolutePath().toString());
164+
IdentityPoolCredentialSource credentialSource =
165+
new IdentityPoolCredentialSource(credentialSourceMap);
166+
167+
FileIdentityPoolSubjectTokenSupplier supplier =
168+
new FileIdentityPoolSubjectTokenSupplier(credentialSource);
169+
170+
String parsedToken = supplier.getSubjectToken(null);
171+
assertEquals(expectedToken, parsedToken);
172+
}
173+
174+
@Test
175+
void getSubjectToken_jsonFormat_returnsToken(@TempDir Path tempDir) throws IOException {
176+
Path file = tempDir.resolve("token.json");
177+
String json = "{\"subjectToken\":\"json-token-from-file\"}";
178+
Files.write(file, json.getBytes(StandardCharsets.UTF_8));
179+
180+
Map<String, Object> credentialSourceMap = new HashMap<>();
181+
credentialSourceMap.put("file", file.toAbsolutePath().toString());
182+
Map<String, String> formatMap = new HashMap<>();
183+
formatMap.put("type", "json");
184+
formatMap.put("subject_token_field_name", "subjectToken");
185+
credentialSourceMap.put("format", formatMap);
186+
187+
IdentityPoolCredentialSource credentialSource =
188+
new IdentityPoolCredentialSource(credentialSourceMap);
189+
190+
FileIdentityPoolSubjectTokenSupplier supplier =
191+
new FileIdentityPoolSubjectTokenSupplier(credentialSource);
192+
193+
String parsedToken = supplier.getSubjectToken(null);
194+
assertEquals("json-token-from-file", parsedToken);
195+
}
196+
197+
@Test
198+
void getSubjectToken_jsonFormat_missingField_throwsException(@TempDir Path tempDir)
199+
throws IOException {
200+
Path file = tempDir.resolve("token.json");
201+
String json = "{\"otherField\":\"json-token-from-file\"}";
202+
Files.write(file, json.getBytes(StandardCharsets.UTF_8));
203+
204+
Map<String, Object> credentialSourceMap = new HashMap<>();
205+
credentialSourceMap.put("file", file.toAbsolutePath().toString());
206+
Map<String, String> formatMap = new HashMap<>();
207+
formatMap.put("type", "json");
208+
formatMap.put("subject_token_field_name", "subjectToken");
209+
credentialSourceMap.put("format", formatMap);
210+
211+
IdentityPoolCredentialSource credentialSource =
212+
new IdentityPoolCredentialSource(credentialSourceMap);
213+
214+
FileIdentityPoolSubjectTokenSupplier supplier =
215+
new FileIdentityPoolSubjectTokenSupplier(credentialSource);
216+
217+
IOException exception = assertThrows(IOException.class, () -> supplier.getSubjectToken(null));
218+
assertTrue(
219+
exception
220+
.getMessage()
221+
.contains("Error when attempting to read the subject token from the credential file."));
222+
}
223+
224+
private static class TrackingInputStream extends ByteArrayInputStream {
225+
boolean closed = false;
226+
227+
TrackingInputStream(byte[] buf) {
228+
super(buf);
229+
}
230+
231+
@Override
232+
public void close() throws IOException {
233+
closed = true;
234+
super.close();
235+
}
236+
}
237+
}

0 commit comments

Comments
 (0)