forked from remote/oauth2
218 lines
6.3 KiB
Go
218 lines
6.3 KiB
Go
// 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"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
EXECUTABLE_SUPPORTED_MAX_VERSION = 1
|
|
)
|
|
|
|
var baseEnv = os.Environ
|
|
|
|
// runCommand is basically an alias of exec.CommandContext for testing.
|
|
var runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) {
|
|
cmd := exec.CommandContext(ctx, command)
|
|
cmd.Env = env
|
|
|
|
response, err := cmd.Output()
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
return response, err
|
|
}
|
|
|
|
type executableCredentialSource struct {
|
|
Command string
|
|
Timeout time.Duration
|
|
OutputFile string
|
|
ctx context.Context
|
|
config *Config
|
|
}
|
|
|
|
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
|
// It also performs defaulting and type conversions.
|
|
func CreateExecutableCredential(ec ExecutableConfig, config *Config, ctx context.Context) (result executableCredentialSource) {
|
|
result.Command = ec.Command
|
|
if ec.TimeoutMillis == 0 {
|
|
result.Timeout = 30 * time.Second
|
|
} else {
|
|
result.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
|
|
}
|
|
result.OutputFile = ec.OutputFile
|
|
result.ctx = ctx
|
|
result.config = config
|
|
return
|
|
}
|
|
|
|
type subjectTokenResponse struct {
|
|
Version *int `json:"version"`
|
|
Success *bool `json:"success"`
|
|
TokenType *string `json:"token_type"`
|
|
ExpirationTime *int64 `json:"expiration_time"`
|
|
IdToken *string `json:"id_token"`
|
|
SamlResponse *string `json:"saml_response"`
|
|
Code *string `json:"code"`
|
|
Message *string `json:"message"`
|
|
}
|
|
|
|
func parseSubjectToken(response []byte) (string, error) {
|
|
var result subjectTokenResponse
|
|
if err := json.Unmarshal(response, &result); err != nil {
|
|
return "", errors.New("oauth2/google: Unable to parse response JSON.")
|
|
}
|
|
|
|
if result.Version == nil {
|
|
return "", errors.New("oauth2/google: Response missing version field.")
|
|
}
|
|
|
|
if result.Success == nil {
|
|
return "", errors.New("oauth2/google: Response missing success field.")
|
|
}
|
|
|
|
if !*result.Success {
|
|
details := ""
|
|
if result.Code != nil {
|
|
details += fmt.Sprintf("(%v)", *result.Code)
|
|
}
|
|
if result.Message != nil {
|
|
if details != "" {
|
|
details += " "
|
|
}
|
|
details += *result.Message
|
|
}
|
|
if details == "" {
|
|
details = "Unknown Error"
|
|
}
|
|
return "", fmt.Errorf("oauth2/google: Executable returned unsuccessful response: %v.", details)
|
|
}
|
|
|
|
if *result.Version > EXECUTABLE_SUPPORTED_MAX_VERSION {
|
|
return "", fmt.Errorf("oauth2/google: Executable returned unsupported version: %v.", *result.Version)
|
|
}
|
|
|
|
if result.ExpirationTime == nil {
|
|
return "", errors.New("oauth2/google: Response missing expiration_time field.")
|
|
}
|
|
|
|
if result.TokenType == nil {
|
|
return "", errors.New("oauth2/google: Response missing token_type field.")
|
|
}
|
|
|
|
if *result.ExpirationTime < now().Unix() {
|
|
return "", errors.New("oauth2/google: The token returned by the executable is expired.")
|
|
}
|
|
|
|
if *result.TokenType == "urn:ietf:params:oauth:token-type:jwt" || *result.TokenType == "urn:ietf:params:oauth:token-type:id_token" {
|
|
if result.IdToken == nil {
|
|
return "", errors.New("oauth2/google: Response missing id_token field.")
|
|
}
|
|
return *result.IdToken, nil
|
|
}
|
|
|
|
if *result.TokenType == "urn:ietf:params:oauth:token-type:saml2" {
|
|
if result.SamlResponse == nil {
|
|
return "", errors.New("oauth2/google: Response missing saml_response field.")
|
|
}
|
|
return *result.SamlResponse, nil
|
|
}
|
|
|
|
return "", errors.New("Executable returned unsupported token type.")
|
|
}
|
|
|
|
func (cs executableCredentialSource) subjectToken() (string, error) {
|
|
if token, ok := cs.getTokenFromInMemoryCaching(); ok {
|
|
return token, nil
|
|
}
|
|
|
|
if token, ok := cs.getTokenFromOutputFile(); ok {
|
|
return token, nil
|
|
}
|
|
|
|
return cs.getTokenFromExecutableCommand()
|
|
}
|
|
|
|
func (cs executableCredentialSource) getTokenFromInMemoryCaching() (string, bool) {
|
|
// TODO
|
|
return "", false
|
|
}
|
|
|
|
func (cs executableCredentialSource) getTokenFromOutputFile() (string, bool) {
|
|
// TODO
|
|
return "", false
|
|
}
|
|
|
|
func (cs executableCredentialSource) getEnvironment() []string {
|
|
result := baseEnv()
|
|
for k, v := range cs.getNewEnvironmentVariables() {
|
|
result = append(result, fmt.Sprintf("%v=%v", k, v))
|
|
}
|
|
return result
|
|
}
|
|
|
|
var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
|
|
|
func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]string {
|
|
result := map[string]string{
|
|
"GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience,
|
|
"GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": cs.config.SubjectTokenType,
|
|
}
|
|
|
|
if cs.config.ServiceAccountImpersonationURL != "" {
|
|
matches := serviceAccountImpersonationCompiler.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL)
|
|
if matches != nil {
|
|
result["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = matches[1]
|
|
}
|
|
}
|
|
|
|
if cs.isInteractive() {
|
|
result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "1"
|
|
} else {
|
|
result["GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"] = "0"
|
|
}
|
|
|
|
if cs.OutputFile != "" {
|
|
result["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = cs.OutputFile
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (cs executableCredentialSource) isInteractive() bool {
|
|
// Currently, executableCredentialSource does not yet support interactive mode.
|
|
return false
|
|
}
|
|
|
|
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 getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" {
|
|
return "", errors.New("Executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run.")
|
|
}
|
|
|
|
ctx, cancel := context.WithDeadline(cs.ctx, now().Add(cs.Timeout))
|
|
defer cancel()
|
|
|
|
if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil {
|
|
if err == context.DeadlineExceeded {
|
|
return "", fmt.Errorf("oauth2/google: executable command timed out.")
|
|
}
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
return "", fmt.Errorf("oauth2/google: executable command failed with exit code %v.", exitError.ExitCode())
|
|
}
|
|
return "", fmt.Errorf("oauth2/google: executable command failed: %v.", err.Error())
|
|
} else {
|
|
return parseSubjectToken(output)
|
|
}
|
|
}
|