From f8a7c0986f001f622e219de0af9028e9bee7462e Mon Sep 17 00:00:00 2001 From: Jeremy Stott Date: Wed, 24 Apr 2019 15:34:01 +1200 Subject: [PATCH] Added TOML cofiguration file support * configuration file located at ~/.aws-oidc/config * sets default parameters, but can still be overridden on the cli * named AuthProviders are accessible via the auth [name] command Renamed exec command to auth. Upgraded auth command to take defaults from the config file. Added new command exec, that puts the temporary credentials as environment variables in the specified command Automatically append URL to end of auth command if not specified --- README.md | 75 ++++++++------ aws-oidc.go | 16 ++- cli/auth.go | 211 ++++++++++++++++++++++++++++++++++++++ cli/exec.go | 234 +++++++++++++++++++------------------------ cli/global.go | 33 ++++-- cli/list.go | 21 +++- config_darwin.go | 7 +- go.mod | 1 + go.sum | 2 + provider/provider.go | 16 ++- 10 files changed, 425 insertions(+), 191 deletions(-) create mode 100644 cli/auth.go diff --git a/README.md b/README.md index c17df41..f50b754 100644 --- a/README.md +++ b/README.md @@ -6,58 +6,69 @@ It outputs temporary AWS credentials in a JSON format that can be consumed by th https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes -OneLogin Example: +Example: - aws-oidc exec \ - --role_arn="arn:aws:iam::892845094662:role/onelogin-test-oidc" \ - --provider_url=https://openid-connect.onelogin.com/oidc \ - --client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 \ - --pkce \ - --nonce \ - -- open -b com.google.chrome -n --args --profile-directory=Default {} + aws-oidc auth \ + --role_arn="arn:aws:iam::892845094662:role/onelogin-test-oidc" \ + --duration=3600 \ + --provider_url=https://openid-connect.onelogin.com/oidc \ + --client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 \ + --agent=open -Google Example: +All the provider arguments can be specified in a TOML configuration file: - aws-oidc exec \ - --role_arn="arn:aws:iam::892845094662:role/web-identity-lanbda" \ - --provider_url=https://accounts.google.com \ - --client_id=430784603061-osbtei3s71l0bj6d8oegto0itefjmiq6.apps.googleusercontent.com \ - --client_secret=... \ - --pkce \ - --nonce \ - -- open -b com.google.chrome -n --args --profile-directory=Default {} + region = "ap-southeast-2" -For some reason, even when using PKCE, google need a client_secret for applications not registered as Android, iOS or Chrome. + [[AuthProvider]] + name = "onelogin" + role_arn = "arn:aws:iam::012345678901:role/role-name" + duration = 900 + provider_url = "https://openid-connect.onelogin.com/oidc" + client_id = "ef061080-43aa-0137-62f3-066d8813aeb888900" + agent = ["open", "-b", "com.google.chrome", "-n", "--args", "--profile-directory=Default", "{}"] + + [[AuthProvider]] + name = "google" + role_arn = "arn:aws:iam::012345678901:role/role-name" + duration = 900 + provider_url = "https://accounts.google.com" + client_id = "430784603061-osbtei3s71l0bj6d8oegto0itefjmiq6.apps.googleusercontent.com" + agent = ["open", "-b", "com.google.chrome", "-n", "--args", "--profile-directory=Profile 1", "{}"] + +This configuration file should be located in **~/.aws-oidc/config** ## Configure AWS Config -~/.aws/config +Add the profiles for each role you want to assume to **~/.aws/config**. Specify the provider name from the configuration file, and override any default settings: - [profile oidc] - credential_process = aws-oidc exec --role_arn=arn:aws:iam::892845094662:role/onelogin-test-oidc --provider_url=https://openid-connect.onelogin.com/oidc --client_id=97a61160-3c09-0137-8c69-0a1c3f4fd822144813 --pkce --nonce -- open -b com.google.chrome -n --args --profile-directory=Default {} + [profile engineer] + credential_process = aws-oidc auth onelogin --role_arn=arn:aws:iam::892845094662:role/onelogin-test-oidc --duration 7200 Now you can use the AWS cli as normal, and specify the profile: - $ aws --profile oidc sts get-caller-identity + $ aws --profile engineer sts get-caller-identity { "UserId": "AROAJUTXNWXGCAEILMXTY:50904038", "Account": "892845094662", "Arn": "arn:aws:sts::892845094662:assumed-role/onelogin-test-oidc/50904038" } -## AWS Cognito +## Run other commands with credentials - ./aws-oidc exec \ - --provider_url=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_eBYNmnpS9 \ - --client_id=70kdnvprlqf1daspkn0iikdngv \ - --pkce \ - --nonce \ - --no-reauth \ - -- open -b com.google.chrome -n --args --profile-directory=Default {} +Most AWS SDK's should be able to pick up the profile parameter, and suppor the `credentials_process` setting in your **~/.aws/config** file. If not, you can run an arbitary command with the temporary credentials with `exec`: + + aws-oidc exec engineer -- ./path/to/command with arguments + +This will use the profiles defined in **~/.aws/config** to assume the role with `aws-oidc` and then set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables for the new process. ## Find roles that an oidc client could assume - aws-vault exec test-privileged-admin -- aws iam list-roles --query < y { + return x + } + return y +} diff --git a/cli/exec.go b/cli/exec.go index 5c23e65..d1523e3 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -1,164 +1,140 @@ package cli import ( - "encoding/json" - "fmt" - - "github.com/99designs/keyring" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/stoggi/aws-oidc/provider" - "golang.org/x/oauth2" + "log" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" "gopkg.in/alecthomas/kingpin.v2" ) +// ExecConfig stores the parameters needed for an exec command type ExecConfig struct { - RoleArn string - Duration int64 - ProviderURL string - ClientID string - ClientSecret string - PKCE bool - Nonce bool - ReAuth bool - AgentCommand []string -} - -// json metadata for AWS credential process. Ref: https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes -type AwsCredentialHelperData struct { - Version int `json:"Version"` - AccessKeyID string `json:"AccessKeyId"` - SecretAccessKey string `json:"SecretAccessKey"` - SessionToken string `json:"SessionToken"` - Expiration string `json:"Expiration,omitempty"` -} - -type LambdaPayload struct { - Role string `json:"role"` - Token string `json:"token"` + Profile string + Command string + Args []string + Signals chan os.Signal } +// ConfigureExec configures the exec command with arguments and flags func ConfigureExec(app *kingpin.Application, config *GlobalConfig) { execConfig := ExecConfig{} - cmd := app.Command("exec", "Execute a command with temporary AWS credentials") + cmd := app.Command("exec", "Retrieve temporary credentials and set them as environment variables") - cmd.Default() + cmd.Arg("profile", "Name of the profile"). + StringVar(&config.Profile) - cmd.Flag("role_arn", "The AWS role you want to assume"). - Required(). - StringVar(&execConfig.RoleArn) + cmd.Arg("cmd", "Command to execute"). + Default(os.Getenv("SHELL")). + StringVar(&execConfig.Command) - cmd.Flag("duration", "The duration to assume the role for in seconds"). - Default("3600"). - Int64Var(&execConfig.Duration) - - cmd.Flag("provider_url", "The OpenID Connect Provider URL"). - Required(). - StringVar(&execConfig.ProviderURL) - - cmd.Flag("client_id", "The OpenID Connect Client ID"). - Required(). - StringVar(&execConfig.ClientID) - - cmd.Flag("client_secret", "The OpenID Connect Client Secret"). - Default(""). - StringVar(&execConfig.ClientSecret) - - cmd.Flag("pkce", "Use PKCE in the OIDC code flow"). - Default("true"). - BoolVar(&execConfig.PKCE) - - cmd.Flag("nonce", "Require a nonce included and verified in the token"). - Default("true"). - BoolVar(&execConfig.Nonce) - - cmd.Flag("reauth", "Require reauthentication by the identity provider"). - Default("false"). - BoolVar(&execConfig.ReAuth) - - cmd.Arg("agent", "The executable and arguments of the local browser to use"). - Default("open", "{}"). - StringsVar(&execConfig.AgentCommand) + cmd.Arg("args", "Command arguments"). + StringsVar(&execConfig.Args) cmd.Action(func(c *kingpin.ParseContext) error { + execConfig.Signals = make(chan os.Signal) ExecCommand(app, config, &execConfig) return nil }) } +// ExecCommand retrieves temporary credentials and sets them as environment variables func ExecCommand(app *kingpin.Application, config *GlobalConfig, execConfig *ExecConfig) { - p := &provider.ProviderConfig{ - ClientID: execConfig.ClientID, - ClientSecret: execConfig.ClientSecret, - ProviderURL: execConfig.ProviderURL, - PKCE: execConfig.PKCE, - Nonce: execConfig.Nonce, - ReAuth: execConfig.ReAuth, - AgentCommand: execConfig.AgentCommand, + if os.Getenv("AWS_OIDC") != "" { + app.Fatalf("aws-vault sessions should be nested with care, unset $AWS_OIDC to force") + return } - oauth2Token := provider.OAuth2Token{} - item, err := (*config.Keyring).Get(execConfig.ClientID) - if err != keyring.ErrKeyNotFound { - if err := json.Unmarshal(item.Data, &oauth2Token); err != nil { - // Log this error only, because we can attempt to recover by getting a new token - app.Errorf("Unable to unmarshal OAuth2Token from keychain: %v", err) + val, err := config.Session.Config.Credentials.Get() + if err != nil { + app.Fatalf("Unable to get credentials for profile: %s", config.Profile) + } + + env := environ(os.Environ()) + env.Set("AWS_OIDC", config.Profile) + + env.Unset("AWS_ACCESS_KEY_ID") + env.Unset("AWS_SECRET_ACCESS_KEY") + env.Unset("AWS_CREDENTIAL_FILE") + env.Unset("AWS_DEFAULT_PROFILE") + env.Unset("AWS_PROFILE") + + if config.Region != "" { + log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region) + env.Set("AWS_DEFAULT_REGION", config.Region) + env.Set("AWS_REGION", config.Region) + } + + log.Println("Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") + env.Set("AWS_ACCESS_KEY_ID", val.AccessKeyID) + env.Set("AWS_SECRET_ACCESS_KEY", val.SecretAccessKey) + + if val.SessionToken != "" { + log.Println("Setting subprocess env: AWS_SESSION_TOKEN, AWS_SECURITY_TOKEN") + env.Set("AWS_SESSION_TOKEN", val.SessionToken) + env.Set("AWS_SECURITY_TOKEN", val.SessionToken) + } + + cmd := exec.Command(execConfig.Command, execConfig.Args...) + cmd.Env = env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + signal.Notify(execConfig.Signals, os.Interrupt, os.Kill) + + if err := cmd.Start(); err != nil { + app.Fatalf("%v", err) + } + // wait for the command to finish + waitCh := make(chan error, 1) + go func() { + waitCh <- cmd.Wait() + close(waitCh) + }() + + for { + select { + case sig := <-execConfig.Signals: + if err = cmd.Process.Signal(sig); err != nil { + app.Errorf("%v", err) + break + } + case err := <-waitCh: + var waitStatus syscall.WaitStatus + if exitError, ok := err.(*exec.ExitError); ok { + waitStatus = exitError.Sys().(syscall.WaitStatus) + os.Exit(waitStatus.ExitStatus()) + } + if err != nil { + app.Fatalf("%v", err) + } + return } } - - err = p.Authenticate(&oauth2Token) - app.FatalIfError(err, "Error authenticating with identity provider") - - AWSCredentialsJSON, err := assumeRoleWithWebIdentity(execConfig, oauth2Token.IDToken) - app.FatalIfError(err, "Error assume role with web identity") - - json, err := json.Marshal(&oauth2Token) - app.FatalIfError(err, "Error marshalling OAuth2 token") - err = (*config.Keyring).Set(keyring.Item{ - Key: execConfig.ClientID, - Data: json, - Label: fmt.Sprintf("OAuth2 token for %s", execConfig.RoleArn), - Description: "OIDC OAuth2 Token", - }) - app.FatalIfError(err, "Error storing OAuth2 Token in keychain") - - fmt.Printf(AWSCredentialsJSON) } -func assumeRoleWithWebIdentity(execConfig *ExecConfig, idToken string) (string, error) { +// environ is a slice of strings representing the environment, in the form "key=value". +type environ []string - svc := sts.New(session.New()) - - input := &sts.AssumeRoleWithWebIdentityInput{ - DurationSeconds: aws.Int64(execConfig.Duration), - RoleArn: aws.String(execConfig.RoleArn), - RoleSessionName: aws.String("aws-oidc"), - WebIdentityToken: aws.String(idToken), +// Unset an environment variable by key +func (e *environ) Unset(key string) { + for i := range *e { + if strings.HasPrefix((*e)[i], key+"=") { + (*e)[i] = (*e)[len(*e)-1] + *e = (*e)[:len(*e)-1] + break + } } - - assumeRoleResult, err := svc.AssumeRoleWithWebIdentity(input) - if err != nil { - return "", err - } - - expiry := *assumeRoleResult.Credentials.Expiration - credentialData := AwsCredentialHelperData{ - Version: 1, - AccessKeyID: *assumeRoleResult.Credentials.AccessKeyId, - SecretAccessKey: *assumeRoleResult.Credentials.SecretAccessKey, - SessionToken: *assumeRoleResult.Credentials.SessionToken, - Expiration: expiry.Format("2006-01-02T15:04:05Z"), - } - - json, err := json.Marshal(&credentialData) - if err != nil { - return "", err - } - - return string(json), nil +} + +// Set adds an environment variable, replacing any existing ones of the same key +func (e *environ) Set(key, val string) { + e.Unset(key) + *e = append(*e, key+"="+val) } diff --git a/cli/global.go b/cli/global.go index 8b868fe..e3f9aab 100644 --- a/cli/global.go +++ b/cli/global.go @@ -1,41 +1,54 @@ package cli import ( - "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" kingpin "gopkg.in/alecthomas/kingpin.v2" ) // GlobalConfig used for defaults and command line arguments type GlobalConfig struct { //Region in AWS used by KMSAuth and BLESS - Region string + Region string + Profile string + AuthProvider []AuthConfig - AwsConfig *vault.Config - Keyring *keyring.Keyring + Session *session.Session + Keyring *keyring.Keyring } // ConfigureGlobal application arguments and flags func ConfigureGlobal(app *kingpin.Application, config *GlobalConfig) { app.Flag("region", "The region in AWS"). - Default("ap-southeast-2"). + Default(config.Region). + Envar("AWS_REGION"). StringVar(&config.Region) + app.Flag("profile", "The profile to use as defined in the AWS config file"). + Default(config.Profile). + Envar("AWS_PROFILE"). + StringVar(&config.Profile) + app.PreAction(func(c *kingpin.ParseContext) (err error) { // Attempt to open the aws-vault keychain keychain, err := keyring.Open(keyring.Config{ - KeychainName: "aws-oidc", - ServiceName: "aws-oidc", - AllowedBackends: []keyring.BackendType{keyring.KeychainBackend}, + KeychainName: "aws-oidc", + ServiceName: "aws-oidc", + AllowedBackends: []keyring.BackendType{keyring.KeychainBackend}, KeychainTrustApplication: true, }) kingpin.FatalIfError(err, "Could not open aws-vault keychain") - - //config.AwsConfig = awsConfig config.Keyring = &keychain + config.Session = session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Region: aws.String(config.Region)}, + Profile: config.Profile, + SharedConfigState: session.SharedConfigEnable, + })) + return nil }) diff --git a/cli/list.go b/cli/list.go index 6c5ede0..2fb97fd 100644 --- a/cli/list.go +++ b/cli/list.go @@ -5,16 +5,18 @@ import ( "fmt" "net/url" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" jmespath "github.com/jmespath/go-jmespath" "gopkg.in/alecthomas/kingpin.v2" ) +// ListConfig stores the parameters needed for a List command type ListConfig struct { ClientID string + Claim string } +// ConfigureList configures the list command with arguments and flags func ConfigureList(app *kingpin.Application, config *GlobalConfig) { listConfig := ListConfig{} @@ -25,15 +27,20 @@ func ConfigureList(app *kingpin.Application, config *GlobalConfig) { Required(). StringVar(&listConfig.ClientID) + cmd.Flag("claim", "The claim used in the IAM policies, prrovider:claim"). + Required(). + StringVar(&listConfig.Claim) + cmd.Action(func(c *kingpin.ParseContext) error { ListCommand(app, config, &listConfig) return nil }) } +// ListCommand retrieves the list of AWS roles that have trust policues that accept a given client_id func ListCommand(app *kingpin.Application, config *GlobalConfig, listConfig *ListConfig) { - svc := iam.New(session.New()) + svc := iam.New(config.Session) input := &iam.ListRolesInput{} listRoleResult, err := svc.ListRoles(input) @@ -47,9 +54,13 @@ func ListCommand(app *kingpin.Application, config *GlobalConfig, listConfig *Lis var d interface{} err = json.Unmarshal([]byte(decodedValue), &d) app.FatalIfError(err, "Unable to unmarshall AssumeRolePolicyDocument") - v, err := jmespath.Search("contains(Statement[].Condition.StringEquals.\"openid-connect.onelogin.com/oidc:aud\", `abc`)", d) - app.FatalIfError(err, "Unable to parse AssumeRolePolicyDocument") - fmt.Println(v) + query := fmt.Sprintf("contains(Statement[].Condition.StringEquals.\"%s\", '%s')", listConfig.Claim, listConfig.ClientID) + containsClientID, err := jmespath.Search(query, d) + app.FatalIfError(err, "Unable to parse AssumeRolePolicyDocument") + if containsClientID.(bool) { + fmt.Println(*role.RoleName) + fmt.Println(*role.Arn) + } } } diff --git a/config_darwin.go b/config_darwin.go index 13a55c7..a4830e3 100644 --- a/config_darwin.go +++ b/config_darwin.go @@ -20,7 +20,12 @@ func execDir() string { return "" } +// GetConfigFilePath returns the path of the configuration file +func GetConfigFilePath() string { + return filepath.Join(homeDir(), ".aws-oidc/config") +} + // GetLogPath returns the path that should be used to store logs func GetLogPath() string { - return filepath.Join(homeDir(), "./Library/Logs/aws-oidc.log") + return filepath.Join(homeDir(), "Library/Logs/aws-oidc.log") } diff --git a/go.mod b/go.mod index ee9463f..989aff9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stoggi/aws-oidc require ( github.com/99designs/aws-vault v4.5.1+incompatible github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f + github.com/BurntSushi/toml v0.3.1 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0 // indirect diff --git a/go.sum b/go.sum index b6064c2..4b122c0 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/99designs/aws-vault v4.5.1+incompatible h1:VjWncFWraO5K5HTRo34YMq2Mkp github.com/99designs/aws-vault v4.5.1+incompatible/go.mod h1:BKt7gBiUkiAOh7TP/c36gMpRJkIk5F8hStyQoWwC/Rw= github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f h1:WXiWWJrYCaOaYimBAXlRdRJ7qOisrYyMLYnCvvhHVms= github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= diff --git a/provider/provider.go b/provider/provider.go index 83e9910..b7dbde6 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -24,7 +24,6 @@ type ProviderConfig struct { ProviderURL string PKCE bool Nonce bool - ReAuth bool AgentCommand []string } @@ -115,12 +114,10 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error { } if t != nil { - if err := t.Refresh(&config); err == nil { - log.Println("Refreshed token successfully") + if err := t.Refresh(&config); err != nil { return nil - } else { - log.Println(err) } + log.Println(err) } stateData := make([]byte, 32) @@ -159,10 +156,6 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error { authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("nonce", nonce)) } - if p.ReAuth { - authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("acr_values", "onelogin:nist:level:1:re-auth")) - } - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { url := config.AuthCodeURL(state, authCodeOptions...) http.Redirect(w, r, url, http.StatusFound) @@ -211,13 +204,18 @@ func (p *ProviderConfig) Authenticate(t *OAuth2Token) error { // Filter the commands, and replace "{}" with our callback url c := p.AgentCommand[:0] + replacedURL := false for _, arg := range p.AgentCommand { if arg == "{}" { c = append(c, baseURL) + replacedURL = true } else { c = append(c, arg) } } + if !replacedURL { + c = append(c, baseURL) + } cmd := exec.Command(c[0], c[1:]...) cmd.Run()