diff --git a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java index d21abed67..d83e541d7 100644 --- a/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java +++ b/oauth2_http/java/com/google/auth/oauth2/PluggableAuthHandler.java @@ -35,7 +35,9 @@ import com.google.api.client.json.JsonParser; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; +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; @@ -139,19 +141,27 @@ public String retrieveTokenFromExecutable(ExecutableOptions options) throws IOEx // location to avoid running the executable until they are expired. ExecutableResponse executableResponse = null; if (options.getOutputFilePath() != null && !options.getOutputFilePath().isEmpty()) { - // Read cached response from output_file. - InputStream inputStream = new FileInputStream(options.getOutputFilePath()); - BufferedReader reader = - new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(reader); - - ExecutableResponse cachedResponse = - new ExecutableResponse(parser.parseAndClose(GenericJson.class)); - - // If the cached response is successful and unexpired, we can use it. - // Response version will be validated below. - if (cachedResponse.isValid()) { - executableResponse = cachedResponse; + // Try reading cached response from output_file. + try { + File outputFile = new File(options.getOutputFilePath()); + // Check if the output file is valid and not empty. + if (outputFile.isFile() && outputFile.length() > 0) { + InputStream inputStream = new FileInputStream(options.getOutputFilePath()); + BufferedReader reader = + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(reader); + ExecutableResponse cachedResponse = + new ExecutableResponse(parser.parseAndClose(GenericJson.class)); + // If the cached response is successful and unexpired, we can use it. + // Response version will be validated below. + if (cachedResponse.isValid()) { + executableResponse = cachedResponse; + } + } + } catch (Exception e) { + throw new PluggableAuthException( + "INVALID_OUTPUT_FILE", + "The output_file specified contains an invalid or malformed response." + e); } } @@ -201,33 +211,42 @@ ExecutableResponse getExecutableResponse(ExecutableOptions options) throws IOExc Process process = processBuilder.start(); ExecutableResponse execResp; + String executableOutput = ""; try { boolean success = process.waitFor(options.getExecutableTimeoutMs(), TimeUnit.MILLISECONDS); if (!success) { // Process has not terminated within the specified timeout. - process.destroyForcibly(); throw new PluggableAuthException( "TIMEOUT_EXCEEDED", "The executable failed to finish within the timeout specified."); } int exitCode = process.exitValue(); if (exitCode != EXIT_CODE_SUCCESS) { - process.destroyForcibly(); throw new PluggableAuthException( "EXIT_CODE", String.format("The executable failed with exit code %s.", exitCode)); } BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); - JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(reader); + executableOutput = CharStreams.toString(reader); + JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(executableOutput); execResp = new ExecutableResponse(parser.parseAndClose(GenericJson.class)); } catch (InterruptedException e) { // Destroy the process. process.destroyForcibly(); throw new PluggableAuthException( "INTERRUPTED", String.format("The execution was interrupted: %s.", e)); + } catch (IOException e) { + // Destroy the process. + process.destroyForcibly(); + if (e instanceof PluggableAuthException) { + throw e; + } + // An error may have occurred in the executable and needs to be surfaced. + throw new PluggableAuthException( + "INVALID_RESPONSE", + String.format("The executable returned an invalid response: %s.", executableOutput)); } - process.destroyForcibly(); return execResp; } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java index 31690ebbc..5233509cf 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthHandlerTest.java @@ -274,6 +274,59 @@ public String getOutputFilePath() { assertEquals(ID_TOKEN, token); } + @Test + void retrieveTokenFromExecutable_withInvalidOutputFile_throws() + throws IOException, InterruptedException { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1"); + + // Build output_file. + File file = File.createTempFile("output_file", /* suffix= */ null, /* directory= */ null); + file.deleteOnExit(); + + OAuth2Utils.writeInputStreamToFile( + new ByteArrayInputStream("Bad response.".getBytes(StandardCharsets.UTF_8)), + file.getAbsolutePath()); + + // Options with output file specified. + ExecutableOptions options = + new ExecutableOptions() { + @Override + public String getExecutableCommand() { + return "/path/to/executable"; + } + + @Override + public Map getEnvironmentMap() { + return ImmutableMap.of(); + } + + @Override + public int getExecutableTimeoutMs() { + return 30000; + } + + @Override + public String getOutputFilePath() { + return file.getAbsolutePath(); + } + }; + + // Mock executable handling that does nothing since we are using the output file. + Process mockProcess = Mockito.mock(Process.class); + InternalProcessBuilder processBuilder = + buildInternalProcessBuilder(new HashMap<>(), mockProcess, options.getExecutableCommand()); + + PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder); + + // Call retrieveTokenFromExecutable(). + PluggableAuthException e = + assertThrows( + PluggableAuthException.class, () -> handler.retrieveTokenFromExecutable(options)); + + assertEquals("INVALID_OUTPUT_FILE", e.getErrorCode()); + } + @Test void retrieveTokenFromExecutable_expiredOutputFileResponse_callsExecutable() throws IOException, InterruptedException { @@ -667,6 +720,43 @@ void getExecutableResponse_processInterrupted_throws() throws InterruptedExcepti verify(mockProcess, times(1)).destroyForcibly(); } + @Test + void getExecutableResponse_invalidResponse_throws() throws InterruptedException { + TestEnvironmentProvider environmentProvider = new TestEnvironmentProvider(); + environmentProvider.setEnv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES", "1"); + + // Mock executable handling. + Process mockProcess = Mockito.mock(Process.class); + when(mockProcess.waitFor(anyLong(), any(TimeUnit.class))).thenReturn(true); + when(mockProcess.exitValue()).thenReturn(EXIT_CODE_SUCCESS); + + // Mock bad executable response. + String badResponse = "badResponse"; + when(mockProcess.getInputStream()) + .thenReturn(new ByteArrayInputStream(badResponse.getBytes(StandardCharsets.UTF_8))); + + InternalProcessBuilder processBuilder = + buildInternalProcessBuilder( + new HashMap<>(), mockProcess, DEFAULT_OPTIONS.getExecutableCommand()); + + PluggableAuthHandler handler = new PluggableAuthHandler(environmentProvider, processBuilder); + + // Call getExecutableResponse(). + PluggableAuthException e = + assertThrows( + PluggableAuthException.class, () -> handler.getExecutableResponse(DEFAULT_OPTIONS)); + + assertEquals("INVALID_RESPONSE", e.getErrorCode()); + assertEquals( + String.format("The executable returned an invalid response: %s.", badResponse), + e.getErrorDescription()); + + verify(mockProcess, times(1)) + .waitFor( + eq(Long.valueOf(DEFAULT_OPTIONS.getExecutableTimeoutMs())), eq(TimeUnit.MILLISECONDS)); + verify(mockProcess, times(1)).destroyForcibly(); + } + private static GenericJson buildOidcResponse() { GenericJson json = new GenericJson(); json.setFactory(OAuth2Utils.JSON_FACTORY);