google/internal/externalaccount: create executable credentials

This changeset would allow users to specify a command to be run which will return a token

Change-Id: If84cce97c273cdd08ef2010a1693cd813d053ed3
GitHub-Last-Rev: 98f37871caf9f21b5d47f197ef7447f6961f5b47
GitHub-Pull-Request: golang/oauth2#563
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/404114
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Cody Oss <codyoss@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go
index 83ce9c2..b3d5fe2 100644
--- a/google/internal/externalaccount/basecredentials.go
+++ b/google/internal/externalaccount/basecredentials.go
@@ -163,7 +163,7 @@
 }
 
 // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
-// Either the File or the URL field should be filled, depending on the kind of credential in question.
+// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
 // The EnvironmentID should start with AWS if being used for an AWS credential.
 type CredentialSource struct {
 	File string `json:"file"`
@@ -171,6 +171,8 @@
 	URL     string            `json:"url"`
 	Headers map[string]string `json:"headers"`
 
+	Executable *ExecutableConfig `json:"executable"`
+
 	EnvironmentID               string `json:"environment_id"`
 	RegionURL                   string `json:"region_url"`
 	RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
@@ -179,7 +181,13 @@
 	Format                      format `json:"format"`
 }
 
-// parse determines the type of CredentialSource needed
+type ExecutableConfig struct {
+	Command       string `json:"command"`
+	TimeoutMillis *int   `json:"timeout_millis"`
+	OutputFile    string `json:"output_file"`
+}
+
+// parse determines the type of CredentialSource needed.
 func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
 	if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
 		if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
@@ -205,6 +213,8 @@
 		return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
 	} else if c.CredentialSource.URL != "" {
 		return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
+	} else if c.CredentialSource.Executable != nil {
+		return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c)
 	}
 	return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
 }
diff --git a/google/internal/externalaccount/executablecredsource.go b/google/internal/externalaccount/executablecredsource.go
new file mode 100644
index 0000000..6ecbe6e
--- /dev/null
+++ b/google/internal/externalaccount/executablecredsource.go
@@ -0,0 +1,308 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package externalaccount
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"regexp"
+	"strings"
+	"time"
+)
+
+var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
+
+const (
+	executableSupportedMaxVersion = 1
+	defaultTimeout                = 30 * time.Second
+	timeoutMinimum                = 5 * time.Second
+	timeoutMaximum                = 120 * time.Second
+	executableSource              = "response"
+	outputFileSource              = "output file"
+)
+
+type nonCacheableError struct {
+	message string
+}
+
+func (nce nonCacheableError) Error() string {
+	return nce.message
+}
+
+func missingFieldError(source, field string) error {
+	return fmt.Errorf("oauth2/google: %v missing `%q` field", source, field)
+}
+
+func jsonParsingError(source, data string) error {
+	return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data)
+}
+
+func malformedFailureError() error {
+	return nonCacheableError{"oauth2/google: response must include `error` and `message` fields when unsuccessful"}
+}
+
+func userDefinedError(code, message string) error {
+	return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", code, message)}
+}
+
+func unsupportedVersionError(source string, version int) error {
+	return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version)
+}
+
+func tokenExpiredError() error {
+	return nonCacheableError{"oauth2/google: the token returned by the executable is expired"}
+}
+
+func tokenTypeError(source string) error {
+	return fmt.Errorf("oauth2/google: %v contains unsupported token type", source)
+}
+
+func exitCodeError(exitCode int) error {
+	return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode)
+}
+
+func executableError(err error) error {
+	return fmt.Errorf("oauth2/google: executable command failed: %v", err)
+}
+
+func executablesDisallowedError() error {
+	return errors.New("oauth2/google: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
+}
+
+func timeoutRangeError() error {
+	return errors.New("oauth2/google: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
+}
+
+func commandMissingError() error {
+	return errors.New("oauth2/google: missing `command` field — executable command must be provided")
+}
+
+type environment interface {
+	existingEnv() []string
+	getenv(string) string
+	run(ctx context.Context, command string, env []string) ([]byte, error)
+	now() time.Time
+}
+
+type runtimeEnvironment struct{}
+
+func (r runtimeEnvironment) existingEnv() []string {
+	return os.Environ()
+}
+
+func (r runtimeEnvironment) getenv(key string) string {
+	return os.Getenv(key)
+}
+
+func (r runtimeEnvironment) now() time.Time {
+	return time.Now().UTC()
+}
+
+func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
+	splitCommand := strings.Fields(command)
+	cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
+	cmd.Env = env
+
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		if ctx.Err() == context.DeadlineExceeded {
+			return nil, context.DeadlineExceeded
+		}
+
+		if exitError, ok := err.(*exec.ExitError); ok {
+			return nil, exitCodeError(exitError.ExitCode())
+		}
+
+		return nil, executableError(err)
+	}
+
+	bytesStdout := bytes.TrimSpace(stdout.Bytes())
+	if len(bytesStdout) > 0 {
+		return bytesStdout, nil
+	}
+	return bytes.TrimSpace(stderr.Bytes()), nil
+}
+
+type executableCredentialSource struct {
+	Command    string
+	Timeout    time.Duration
+	OutputFile string
+	ctx        context.Context
+	config     *Config
+	env        environment
+}
+
+// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
+// It also performs defaulting and type conversions.
+func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
+	if ec.Command == "" {
+		return executableCredentialSource{}, commandMissingError()
+	}
+
+	result := executableCredentialSource{}
+	result.Command = ec.Command
+	if ec.TimeoutMillis == nil {
+		result.Timeout = defaultTimeout
+	} else {
+		result.Timeout = time.Duration(*ec.TimeoutMillis) * time.Millisecond
+		if result.Timeout < timeoutMinimum || result.Timeout > timeoutMaximum {
+			return executableCredentialSource{}, timeoutRangeError()
+		}
+	}
+	result.OutputFile = ec.OutputFile
+	result.ctx = ctx
+	result.config = config
+	result.env = runtimeEnvironment{}
+	return result, nil
+}
+
+type executableResponse struct {
+	Version        int    `json:"version,omitempty"`
+	Success        *bool  `json:"success,omitempty"`
+	TokenType      string `json:"token_type,omitempty"`
+	ExpirationTime int64  `json:"expiration_time,omitempty"`
+	IdToken        string `json:"id_token,omitempty"`
+	SamlResponse   string `json:"saml_response,omitempty"`
+	Code           string `json:"code,omitempty"`
+	Message        string `json:"message,omitempty"`
+}
+
+func parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
+	var result executableResponse
+	if err := json.Unmarshal(response, &result); err != nil {
+		return "", jsonParsingError(source, string(response))
+	}
+
+	if result.Version == 0 {
+		return "", missingFieldError(source, "version")
+	}
+
+	if result.Success == nil {
+		return "", missingFieldError(source, "success")
+	}
+
+	if !*result.Success {
+		if result.Code == "" || result.Message == "" {
+			return "", malformedFailureError()
+		}
+		return "", userDefinedError(result.Code, result.Message)
+	}
+
+	if result.Version > executableSupportedMaxVersion || result.Version < 0 {
+		return "", unsupportedVersionError(source, result.Version)
+	}
+
+	if result.ExpirationTime == 0 {
+		return "", missingFieldError(source, "expiration_time")
+	}
+
+	if result.TokenType == "" {
+		return "", missingFieldError(source, "token_type")
+	}
+
+	if result.ExpirationTime < now {
+		return "", tokenExpiredError()
+	}
+
+	if result.TokenType == "urn:ietf:params:oauth:token-type:jwt" || result.TokenType == "urn:ietf:params:oauth:token-type:id_token" {
+		if result.IdToken == "" {
+			return "", missingFieldError(source, "id_token")
+		}
+		return result.IdToken, nil
+	}
+
+	if result.TokenType == "urn:ietf:params:oauth:token-type:saml2" {
+		if result.SamlResponse == "" {
+			return "", missingFieldError(source, "saml_response")
+		}
+		return result.SamlResponse, nil
+	}
+
+	return "", tokenTypeError(source)
+}
+
+func (cs executableCredentialSource) subjectToken() (string, error) {
+	if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
+		return token, err
+	}
+
+	return cs.getTokenFromExecutableCommand()
+}
+
+func (cs executableCredentialSource) getTokenFromOutputFile() (token string, err error) {
+	if cs.OutputFile == "" {
+		// This ExecutableCredentialSource doesn't use an OutputFile.
+		return "", nil
+	}
+
+	file, err := os.Open(cs.OutputFile)
+	if err != nil {
+		// No OutputFile found. Hasn't been created yet, so skip it.
+		return "", nil
+	}
+	defer file.Close()
+
+	data, err := io.ReadAll(io.LimitReader(file, 1<<20))
+	if err != nil || len(data) == 0 {
+		// Cachefile exists, but no data found. Get new credential.
+		return "", nil
+	}
+
+	token, err = parseSubjectTokenFromSource(data, outputFileSource, cs.env.now().Unix())
+	if err != nil {
+		if _, ok := err.(nonCacheableError); ok {
+			// If the cached token is expired we need a new token,
+			// and if the cache contains a failure, we need to try again.
+			return "", nil
+		}
+
+		// There was an error in the cached token, and the developer should be aware of it.
+		return "", err
+	}
+	// Token parsing succeeded.  Use found token.
+	return token, nil
+}
+
+func (cs executableCredentialSource) executableEnvironment() []string {
+	result := cs.env.existingEnv()
+	result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", cs.config.Audience))
+	result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", cs.config.SubjectTokenType))
+	result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
+	if cs.config.ServiceAccountImpersonationURL != "" {
+		matches := serviceAccountImpersonationRE.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL)
+		if matches != nil {
+			result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
+		}
+	}
+	if cs.OutputFile != "" {
+		result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", cs.OutputFile))
+	}
+	return result
+}
+
+func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, error) {
+	// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
+	if cs.env.getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" {
+		return "", executablesDisallowedError()
+	}
+
+	ctx, cancel := context.WithDeadline(cs.ctx, cs.env.now().Add(cs.Timeout))
+	defer cancel()
+
+	output, err := cs.env.run(ctx, cs.Command, cs.executableEnvironment())
+	if err != nil {
+		return "", err
+	}
+	return parseSubjectTokenFromSource(output, executableSource, cs.env.now().Unix())
+}
diff --git a/google/internal/externalaccount/executablecredsource_test.go b/google/internal/externalaccount/executablecredsource_test.go
new file mode 100644
index 0000000..f115b29
--- /dev/null
+++ b/google/internal/externalaccount/executablecredsource_test.go
@@ -0,0 +1,1020 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package externalaccount
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"sort"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+type testEnvironment struct {
+	envVars      map[string]string
+	deadline     time.Time
+	deadlineSet  bool
+	byteResponse []byte
+	jsonResponse *executableResponse
+}
+
+var executablesAllowed = map[string]string{
+	"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": "1",
+}
+
+func (t *testEnvironment) existingEnv() []string {
+	result := []string{}
+	for k, v := range t.envVars {
+		result = append(result, fmt.Sprintf("%v=%v", k, v))
+	}
+	return result
+}
+
+func (t *testEnvironment) getenv(key string) string {
+	return t.envVars[key]
+}
+
+func (t *testEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
+	t.deadline, t.deadlineSet = ctx.Deadline()
+	if t.jsonResponse != nil {
+		return json.Marshal(t.jsonResponse)
+	}
+	return t.byteResponse, nil
+}
+
+func (t *testEnvironment) getDeadline() (time.Time, bool) {
+	return t.deadline, t.deadlineSet
+}
+
+func (t *testEnvironment) now() time.Time {
+	return defaultTime
+}
+
+func Bool(b bool) *bool {
+	return &b
+}
+
+func Int(i int) *int {
+	return &i
+}
+
+var creationTests = []struct {
+	name             string
+	executableConfig ExecutableConfig
+	expectedErr      error
+	expectedTimeout  time.Duration
+}{
+	{
+		name: "Basic Creation",
+		executableConfig: ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(50000),
+		},
+		expectedTimeout: 50000 * time.Millisecond,
+	},
+	{
+		name: "Without Timeout",
+		executableConfig: ExecutableConfig{
+			Command: "blarg",
+		},
+		expectedTimeout: 30000 * time.Millisecond,
+	},
+	{
+		name:             "Without Command",
+		executableConfig: ExecutableConfig{},
+		expectedErr:      commandMissingError(),
+	},
+	{
+		name: "Timeout Too Low",
+		executableConfig: ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(4999),
+		},
+		expectedErr: timeoutRangeError(),
+	},
+	{
+		name: "Timeout Lower Bound",
+		executableConfig: ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(5000),
+		},
+		expectedTimeout: 5000 * time.Millisecond,
+	},
+	{
+		name: "Timeout Upper Bound",
+		executableConfig: ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(120000),
+		},
+		expectedTimeout: 120000 * time.Millisecond,
+	},
+	{
+		name: "Timeout Too High",
+		executableConfig: ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(120001),
+		},
+		expectedErr: timeoutRangeError(),
+	},
+}
+
+func TestCreateExecutableCredential(t *testing.T) {
+	for _, tt := range creationTests {
+		t.Run(tt.name, func(t *testing.T) {
+			ecs, err := CreateExecutableCredential(context.Background(), &tt.executableConfig, nil)
+			if tt.expectedErr != nil {
+				if err == nil {
+					t.Fatalf("Expected error but found none")
+				}
+				if got, want := err.Error(), tt.expectedErr.Error(); got != want {
+					t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want)
+				}
+			} else if err != nil {
+				ecJson := "{???}"
+				if ecBytes, err2 := json.Marshal(tt.executableConfig); err2 != nil {
+					ecJson = string(ecBytes)
+				}
+
+				t.Fatalf("CreateExecutableCredential with %v returned error: %v", ecJson, err)
+			} else {
+				if ecs.Command != "blarg" {
+					t.Errorf("ecs.Command got %v but want %v", ecs.Command, "blarg")
+				}
+				if ecs.Timeout != tt.expectedTimeout {
+					t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout)
+				}
+			}
+		})
+	}
+}
+
+var getEnvironmentTests = []struct {
+	name                string
+	config              Config
+	environment         testEnvironment
+	expectedEnvironment []string
+}{
+	{
+		name: "Minimal Executable Config",
+		config: Config{
+			Audience:         "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
+			CredentialSource: CredentialSource{
+				Executable: &ExecutableConfig{
+					Command: "blarg",
+				},
+			},
+		},
+		environment: testEnvironment{
+			envVars: map[string]string{
+				"A": "B",
+			},
+		},
+		expectedEnvironment: []string{
+			"A=B",
+			"GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			"GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt",
+			"GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0",
+		},
+	},
+	{
+		name: "Full Impersonation URL",
+		config: Config{
+			Audience:                       "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
+			SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
+			CredentialSource: CredentialSource{
+				Executable: &ExecutableConfig{
+					Command:    "blarg",
+					OutputFile: "/path/to/generated/cached/credentials",
+				},
+			},
+		},
+		environment: testEnvironment{
+			envVars: map[string]string{
+				"A": "B",
+			},
+		},
+		expectedEnvironment: []string{
+			"A=B",
+			"GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			"GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt",
+			"GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=test@project.iam.gserviceaccount.com",
+			"GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0",
+			"GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials",
+		},
+	},
+	{
+		name: "Impersonation Email",
+		config: Config{
+			Audience:                       "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
+			SubjectTokenType:               "urn:ietf:params:oauth:token-type:jwt",
+			CredentialSource: CredentialSource{
+				Executable: &ExecutableConfig{
+					Command:    "blarg",
+					OutputFile: "/path/to/generated/cached/credentials",
+				},
+			},
+		},
+		environment: testEnvironment{
+			envVars: map[string]string{
+				"A": "B",
+			},
+		},
+		expectedEnvironment: []string{
+			"A=B",
+			"GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
+			"GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:jwt",
+			"GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0",
+			"GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=/path/to/generated/cached/credentials",
+		},
+	},
+}
+
+func TestExecutableCredentialGetEnvironment(t *testing.T) {
+	for _, tt := range getEnvironmentTests {
+		t.Run(tt.name, func(t *testing.T) {
+			config := tt.config
+
+			ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config)
+			if err != nil {
+				t.Fatalf("creation failed %v", err)
+			}
+
+			ecs.env = &tt.environment
+
+			// This Transformer sorts a []string.
+			sorter := cmp.Transformer("Sort", func(in []string) []string {
+				out := append([]string(nil), in...) // Copy input to avoid mutating it
+				sort.Strings(out)
+				return out
+			})
+
+			if got, want := ecs.executableEnvironment(), tt.expectedEnvironment; !cmp.Equal(got, want, sorter) {
+				t.Errorf("Incorrect environment received.\nReceived: %s\nExpected: %s", got, want)
+			}
+		})
+	}
+}
+
+var failureTests = []struct {
+	name            string
+	testEnvironment testEnvironment
+	noExecution     bool
+	expectedErr     error
+}{
+	{
+		name: "Environment Variable Not Set",
+		testEnvironment: testEnvironment{
+			byteResponse: []byte{},
+		},
+		noExecution: true,
+		expectedErr: executablesDisallowedError(),
+	},
+
+	{
+		name: "Invalid Token",
+		testEnvironment: testEnvironment{
+			envVars:      executablesAllowed,
+			byteResponse: []byte("tokentokentoken"),
+		},
+		expectedErr: jsonParsingError(executableSource, "tokentokentoken"),
+	},
+
+	{
+		name: "Version Field Missing",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(true),
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "version"),
+	},
+
+	{
+		name: "Success Field Missing",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Version: 1,
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "success"),
+	},
+
+	{
+		name: "User defined error",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(false),
+				Version: 1,
+				Code:    "404",
+				Message: "Token Not Found",
+			},
+		},
+		expectedErr: userDefinedError("404", "Token Not Found"),
+	},
+
+	{
+		name: "User defined error without code",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(false),
+				Version: 1,
+				Message: "Token Not Found",
+			},
+		},
+		expectedErr: malformedFailureError(),
+	},
+
+	{
+		name: "User defined error without message",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(false),
+				Version: 1,
+				Code:    "404",
+			},
+		},
+		expectedErr: malformedFailureError(),
+	},
+
+	{
+		name: "User defined error without fields",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(false),
+				Version: 1,
+			},
+		},
+		expectedErr: malformedFailureError(),
+	},
+
+	{
+		name: "Newer Version",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success: Bool(true),
+				Version: 2,
+			},
+		},
+		expectedErr: unsupportedVersionError(executableSource, 2),
+	},
+
+	{
+		name: "Missing Token Type",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix(),
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "token_type"),
+	},
+
+	{
+		name: "Missing Expiration",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:   Bool(true),
+				Version:   1,
+				TokenType: "urn:ietf:params:oauth:token-type:jwt",
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "expiration_time"),
+	},
+
+	{
+		name: "Token Expired",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix() - 1,
+				TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+			},
+		},
+		expectedErr: tokenExpiredError(),
+	},
+
+	{
+		name: "Invalid Token Type",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix(),
+				TokenType:      "urn:ietf:params:oauth:token-type:invalid",
+			},
+		},
+		expectedErr: tokenTypeError(executableSource),
+	},
+
+	{
+		name: "Missing JWT",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix(),
+				TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "id_token"),
+	},
+
+	{
+		name: "Missing ID Token",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix(),
+				TokenType:      "urn:ietf:params:oauth:token-type:id_token",
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "id_token"),
+	},
+
+	{
+		name: "Missing SAML Token",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix(),
+				TokenType:      "urn:ietf:params:oauth:token-type:saml2",
+			},
+		},
+		expectedErr: missingFieldError(executableSource, "saml_response"),
+	},
+}
+
+func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) {
+	cs := CredentialSource{
+		Executable: &ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(5000),
+		},
+	}
+
+	tfc := testFileConfig
+	tfc.CredentialSource = cs
+
+	base, err := tfc.parse(context.Background())
+	if err != nil {
+		t.Fatalf("parse() failed %v", err)
+	}
+
+	ecs, ok := base.(executableCredentialSource)
+	if !ok {
+		t.Fatalf("Wrong credential type created.")
+	}
+
+	for _, tt := range failureTests {
+		t.Run(tt.name, func(t *testing.T) {
+			ecs.env = &tt.testEnvironment
+
+			if _, err = ecs.subjectToken(); err == nil {
+				t.Fatalf("Expected error but found none")
+			} else if got, want := err.Error(), tt.expectedErr.Error(); got != want {
+				t.Errorf("Incorrect error received.\nReceived: %s\nExpected: %s", got, want)
+			}
+
+			deadline, deadlineSet := tt.testEnvironment.getDeadline()
+			if tt.noExecution {
+				if deadlineSet {
+					t.Errorf("Executable called when it should not have been")
+				}
+			} else {
+				if !deadlineSet {
+					t.Errorf("Command run without a deadline")
+				} else if deadline != defaultTime.Add(5*time.Second) {
+					t.Errorf("Command run with incorrect deadline")
+				}
+			}
+		})
+	}
+}
+
+var successTests = []struct {
+	name            string
+	testEnvironment testEnvironment
+}{
+	{
+		name: "JWT",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix() + 3600,
+				TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+				IdToken:        "tokentokentoken",
+			},
+		},
+	},
+
+	{
+		name: "ID Token",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix() + 3600,
+				TokenType:      "urn:ietf:params:oauth:token-type:id_token",
+				IdToken:        "tokentokentoken",
+			},
+		},
+	},
+
+	{
+		name: "SAML",
+		testEnvironment: testEnvironment{
+			envVars: executablesAllowed,
+			jsonResponse: &executableResponse{
+				Success:        Bool(true),
+				Version:        1,
+				ExpirationTime: defaultTime.Unix() + 3600,
+				TokenType:      "urn:ietf:params:oauth:token-type:saml2",
+				SamlResponse:   "tokentokentoken",
+			},
+		},
+	},
+}
+
+func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) {
+	cs := CredentialSource{
+		Executable: &ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(5000),
+		},
+	}
+
+	tfc := testFileConfig
+	tfc.CredentialSource = cs
+
+	base, err := tfc.parse(context.Background())
+	if err != nil {
+		t.Fatalf("parse() failed %v", err)
+	}
+
+	ecs, ok := base.(executableCredentialSource)
+	if !ok {
+		t.Fatalf("Wrong credential type created.")
+	}
+
+	for _, tt := range successTests {
+		t.Run(tt.name, func(t *testing.T) {
+			ecs.env = &tt.testEnvironment
+
+			out, err := ecs.subjectToken()
+			if err != nil {
+				t.Fatalf("retrieveSubjectToken() failed: %v", err)
+			}
+
+			deadline, deadlineSet := tt.testEnvironment.getDeadline()
+			if !deadlineSet {
+				t.Errorf("Command run without a deadline")
+			} else if deadline != defaultTime.Add(5*time.Second) {
+				t.Errorf("Command run with incorrect deadline")
+			}
+
+			if got, want := out, "tokentokentoken"; got != want {
+				t.Errorf("Incorrect token received.\nReceived: %s\nExpected: %s", got, want)
+			}
+		})
+	}
+}
+
+func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) {
+	outputFile, err := ioutil.TempFile("testdata", "result.*.json")
+	if err != nil {
+		t.Fatalf("Tempfile failed: %v", err)
+	}
+	defer os.Remove(outputFile.Name())
+
+	cs := CredentialSource{
+		Executable: &ExecutableConfig{
+			Command:       "blarg",
+			TimeoutMillis: Int(5000),
+			OutputFile:    outputFile.Name(),
+		},
+	}
+
+	tfc := testFileConfig
+	tfc.CredentialSource = cs
+
+	base, err := tfc.parse(context.Background())
+	if err != nil {
+		t.Fatalf("parse() failed %v", err)
+	}
+
+	ecs, ok := base.(executableCredentialSource)
+	if !ok {
+		t.Fatalf("Wrong credential type created.")
+	}
+
+	if _, err = outputFile.Write([]byte("tokentokentoken")); err != nil {
+		t.Fatalf("error writing to file: %v", err)
+	}
+
+	te := testEnvironment{
+		envVars:      executablesAllowed,
+		byteResponse: []byte{},
+	}
+	ecs.env = &te
+
+	if _, err = base.subjectToken(); err == nil {
+		t.Fatalf("Expected error but found none")
+	} else if got, want := err.Error(), jsonParsingError(outputFileSource, "tokentokentoken").Error(); got != want {
+		t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
+	}
+
+	_, deadlineSet := te.getDeadline()
+	if deadlineSet {
+		t.Errorf("Executable called when it should not have been")
+	}
+}
+
+// These are errors in the output file that should be reported to the user.
+// Most of these will help the developers debug their code.
+var cacheFailureTests = []struct {
+	name               string
+	outputFileContents executableResponse
+	expectedErr        error
+}{
+	{
+		name: "Missing Version",
+		outputFileContents: executableResponse{
+			Success: Bool(true),
+		},
+		expectedErr: missingFieldError(outputFileSource, "version"),
+	},
+
+	{
+		name: "Missing Success",
+		outputFileContents: executableResponse{
+			Version: 1,
+		},
+		expectedErr: missingFieldError(outputFileSource, "success"),
+	},
+
+	{
+		name: "Newer Version",
+		outputFileContents: executableResponse{
+			Success: Bool(true),
+			Version: 2,
+		},
+		expectedErr: unsupportedVersionError(outputFileSource, 2),
+	},
+
+	{
+		name: "Missing Token Type",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix(),
+		},
+		expectedErr: missingFieldError(outputFileSource, "token_type"),
+	},
+
+	{
+		name: "Missing Expiration",
+		outputFileContents: executableResponse{
+			Success:   Bool(true),
+			Version:   1,
+			TokenType: "urn:ietf:params:oauth:token-type:jwt",
+		},
+		expectedErr: missingFieldError(outputFileSource, "expiration_time"),
+	},
+
+	{
+		name: "Invalid Token Type",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix(),
+			TokenType:      "urn:ietf:params:oauth:token-type:invalid",
+		},
+		expectedErr: tokenTypeError(outputFileSource),
+	},
+
+	{
+		name: "Missing JWT",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+		},
+		expectedErr: missingFieldError(outputFileSource, "id_token"),
+	},
+
+	{
+		name: "Missing ID Token",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:id_token",
+		},
+		expectedErr: missingFieldError(outputFileSource, "id_token"),
+	},
+
+	{
+		name: "Missing SAML",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+		},
+		expectedErr: missingFieldError(outputFileSource, "id_token"),
+	},
+}
+
+func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) {
+	for _, tt := range cacheFailureTests {
+		t.Run(tt.name, func(t *testing.T) {
+			outputFile, err := ioutil.TempFile("testdata", "result.*.json")
+			if err != nil {
+				t.Fatalf("Tempfile failed: %v", err)
+			}
+			defer os.Remove(outputFile.Name())
+
+			cs := CredentialSource{
+				Executable: &ExecutableConfig{
+					Command:       "blarg",
+					TimeoutMillis: Int(5000),
+					OutputFile:    outputFile.Name(),
+				},
+			}
+
+			tfc := testFileConfig
+			tfc.CredentialSource = cs
+
+			base, err := tfc.parse(context.Background())
+			if err != nil {
+				t.Fatalf("parse() failed %v", err)
+			}
+
+			ecs, ok := base.(executableCredentialSource)
+			if !ok {
+				t.Fatalf("Wrong credential type created.")
+			}
+			te := testEnvironment{
+				envVars:      executablesAllowed,
+				byteResponse: []byte{},
+			}
+			ecs.env = &te
+			if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil {
+				t.Errorf("Error encoding to file: %v", err)
+				return
+			}
+			if _, err = ecs.subjectToken(); err == nil {
+				t.Errorf("Expected error but found none")
+			} else if got, want := err.Error(), tt.expectedErr.Error(); got != want {
+				t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
+			}
+
+			if _, deadlineSet := te.getDeadline(); deadlineSet {
+				t.Errorf("Executable called when it should not have been")
+			}
+		})
+	}
+}
+
+// These tests should ignore the error in the output file, and check the executable.
+var invalidCacheTests = []struct {
+	name               string
+	outputFileContents executableResponse
+}{
+	{
+		name: "User Defined Error",
+		outputFileContents: executableResponse{
+			Success: Bool(false),
+			Version: 1,
+			Code:    "404",
+			Message: "Token Not Found",
+		},
+	},
+
+	{
+		name: "User Defined Error without Code",
+		outputFileContents: executableResponse{
+			Success: Bool(false),
+			Version: 1,
+			Message: "Token Not Found",
+		},
+	},
+
+	{
+		name: "User Defined Error without Message",
+		outputFileContents: executableResponse{
+			Success: Bool(false),
+			Version: 1,
+			Code:    "404",
+		},
+	},
+
+	{
+		name: "User Defined Error without Fields",
+		outputFileContents: executableResponse{
+			Success: Bool(false),
+			Version: 1,
+		},
+	},
+
+	{
+		name: "Expired Token",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() - 1,
+			TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+		},
+	},
+}
+
+func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) {
+	for _, tt := range invalidCacheTests {
+		t.Run(tt.name, func(t *testing.T) {
+			outputFile, err := ioutil.TempFile("testdata", "result.*.json")
+			if err != nil {
+				t.Fatalf("Tempfile failed: %v", err)
+			}
+			defer os.Remove(outputFile.Name())
+
+			cs := CredentialSource{
+				Executable: &ExecutableConfig{
+					Command:       "blarg",
+					TimeoutMillis: Int(5000),
+					OutputFile:    outputFile.Name(),
+				},
+			}
+
+			tfc := testFileConfig
+			tfc.CredentialSource = cs
+
+			base, err := tfc.parse(context.Background())
+			if err != nil {
+				t.Fatalf("parse() failed %v", err)
+			}
+
+			te := testEnvironment{
+				envVars: executablesAllowed,
+				jsonResponse: &executableResponse{
+					Success:        Bool(true),
+					Version:        1,
+					ExpirationTime: defaultTime.Unix() + 3600,
+					TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+					IdToken:        "tokentokentoken",
+				},
+			}
+
+			ecs, ok := base.(executableCredentialSource)
+			if !ok {
+				t.Fatalf("Wrong credential type created.")
+			}
+			ecs.env = &te
+
+			if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil {
+				t.Errorf("Error encoding to file: %v", err)
+				return
+			}
+
+			out, err := ecs.subjectToken()
+			if err != nil {
+				t.Errorf("retrieveSubjectToken() failed: %v", err)
+				return
+			}
+
+			if deadline, deadlineSet := te.getDeadline(); !deadlineSet {
+				t.Errorf("Command run without a deadline")
+			} else if deadline != defaultTime.Add(5*time.Second) {
+				t.Errorf("Command run with incorrect deadline")
+			}
+
+			if got, want := out, "tokentokentoken"; got != want {
+				t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got)
+			}
+		})
+	}
+}
+
+var cacheSuccessTests = []struct {
+	name               string
+	outputFileContents executableResponse
+}{
+	{
+		name: "JWT",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:jwt",
+			IdToken:        "tokentokentoken",
+		},
+	},
+
+	{
+		name: "Id Token",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:id_token",
+			IdToken:        "tokentokentoken",
+		},
+	},
+
+	{
+		name: "SAML",
+		outputFileContents: executableResponse{
+			Success:        Bool(true),
+			Version:        1,
+			ExpirationTime: defaultTime.Unix() + 3600,
+			TokenType:      "urn:ietf:params:oauth:token-type:saml2",
+			SamlResponse:   "tokentokentoken",
+		},
+	},
+}
+
+func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
+	for _, tt := range cacheSuccessTests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			outputFile, err := ioutil.TempFile("testdata", "result.*.json")
+			if err != nil {
+				t.Fatalf("Tempfile failed: %v", err)
+			}
+			defer os.Remove(outputFile.Name())
+
+			cs := CredentialSource{
+				Executable: &ExecutableConfig{
+					Command:       "blarg",
+					TimeoutMillis: Int(5000),
+					OutputFile:    outputFile.Name(),
+				},
+			}
+
+			tfc := testFileConfig
+			tfc.CredentialSource = cs
+
+			base, err := tfc.parse(context.Background())
+			if err != nil {
+				t.Fatalf("parse() failed %v", err)
+			}
+
+			te := testEnvironment{
+				envVars:      executablesAllowed,
+				byteResponse: []byte{},
+			}
+
+			ecs, ok := base.(executableCredentialSource)
+			if !ok {
+				t.Fatalf("Wrong credential type created.")
+			}
+			ecs.env = &te
+
+			if err = json.NewEncoder(outputFile).Encode(tt.outputFileContents); err != nil {
+				t.Errorf("Error encoding to file: %v", err)
+				return
+			}
+
+			if out, err := ecs.subjectToken(); err != nil {
+				t.Errorf("retrieveSubjectToken() failed: %v", err)
+			} else if got, want := out, "tokentokentoken"; got != want {
+				t.Errorf("Incorrect token received.\nExpected: %s\nRecieved: %s", want, got)
+			}
+
+			if _, deadlineSet := te.getDeadline(); deadlineSet {
+				t.Errorf("Executable called when it should not have been")
+			}
+		})
+	}
+}