Skip to content

Commit

Permalink
Merge pull request #143 from okta/m2m_process_credentials
Browse files Browse the repository at this point in the history
AWS CLI process credentials JSON output
  • Loading branch information
MikeMondragon-okta authored Oct 2, 2023
2 parents 88ac9f5 + ff7650f commit 1269a4b
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 103 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ in the naming convention for `okta-aws-cli` specific names.
| `OKTA_OIDC_CLIENT_ID` | `OKTA_AWSCLI_OIDC_CLIENT_ID` |
| `OKTA_AWS_ACCOUNT_FEDERATION_APP_ID` | `OKTA_AWSCLI_AWS_ACCOUNT_FEDERATION_APP_ID` |

### (expected) Credentials output as JSON
### (Completed) Process credential provider output as JSON

Emits IAM temporary credentials as JSON in [process
credentials](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html)
Expand Down Expand Up @@ -71,6 +71,17 @@ $ okta-aws-cli web \
--open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --profile-directory='Profile 1'"
```

## 2.0.0-beta.1 (October 2, 2023)

Support for AWS CLI [process credential provider](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html)

```
# $/.aws/config
[default]
# presumes OKTA_AWSCLI_* env vars are set
credential_process = okta-aws-cli m2m --format process-credentials
```

## 2.0.0-beta.0 (September 29, 2023)

### New commands
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@ These global settings are optional unless marked otherwise:
| Cache Okta access token at `$HOME/.okta/awscli-access-token.json` to reduce need to open device authorization URL | `true` if flag is present | `--cache-access-token` | `OKTA_AWSCLI_CACHE_ACCESS_TOKEN=true` |
| Alternate AWS credentials file path | Path to alternative credentials file other than AWS CLI default | `--aws-credentials` | `OKTA_AWSCLI_AWS_CREDENTIALS` |
| (Over)write the given profile to the AWS credentials file. WARNING: When enabled, overwriting can inadvertently remove dangling comments and extraneous formatting from the creds file. | `true` if flag is present | `--write-aws-credentials` | `OKTA_AWSCLI_WRITE_AWS_CREDENTIALS=true` |
| Emit deprecated AWS variable `aws_security_token` with duplicated value from `aws_session_token` | `true` if flag is present | `--legacy-aws-variables` | `OKTA_AWSCLI_LEGACY_AWS_VARIABLES=true` |
| Emit expiry timestamp `x_security_token_expires` in RFC3339 format for the session/security token (AWS credentials file only) | `true` if flag is present | `--expiry-aws-variables` | `OKTA_AWSCLI_EXPIRY_AWS_VARIABLES=true` |
| Emit deprecated AWS variable `aws_security_token` with duplicated value from `aws_session_token`. AWS CLI removed any reference and documentation for `aws_security_token` in November 2014. | `true` if flag is present | `--legacy-aws-variables` | `OKTA_AWSCLI_LEGACY_AWS_VARIABLES=true` |
| Emit expiry timestamp `x_security_token_expires` in RFC3339 format for the session/security token (AWS credentials file only). This is a non-standard profile variable. | `true` if flag is present | `--expiry-aws-variables` | `OKTA_AWSCLI_EXPIRY_AWS_VARIABLES=true` |
| Print operational information to the screen for debugging purposes | `true` if flag is present | `--debug` | `OKTA_AWSCLI_DEBUG=true` |
| Verbosely print all API calls/responses to the screen | `true` if flag is present | `--debug-api-calls` | `OKTA_AWSCLI_DEBUG_API_CALLS=true` |
| HTTP/HTTPS Proxy support | HTTP/HTTPS URL of proxy service (based on golang [net/http/httpproxy](https://pkg.go.dev/golang.org/x/net/http/httpproxy) package) | n/a | `HTTP_PROXY` or `HTTPS_PROXY` |
Expand Down Expand Up @@ -679,6 +679,23 @@ aws --profile example s3 ls
Unable to parse config file: /home/user/.aws/credentials
```

### Process credentials provider

`okta-aws-cli` supports JSON output for the AWS CLI [credential process
argument](https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html).
Add this line to the `default` section of `$/.aws/config`. First m2m example
presumes `m2m` arguments are in `OKTA_AWSCLI_*` environment variables, AWS CLI
passes those through. Second web example has args spelled out directly in the
credential process values.

M2M example:

`credential_process = okta-aws-cli m2m --format process-credentials`

Web example:

`credential_process = okta-aws-cli web --format process-credentials --oidc-client-id abc --org-domain test.okat.com --aws-iam-idp arn:aws:iam::123:saml-provider/my-idp --aws-iam-role arn:aws:iam::294719231913:role/s3 --open-browser`

### Help

```shell
Expand Down
2 changes: 1 addition & 1 deletion cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func init() {
Name: config.FormatFlag,
Short: "f",
Value: "",
Usage: "Output format. [env-var|aws-credentials]",
Usage: "Output format. [env-var|aws-credentials|process-credentials]",
EnvVar: config.FormatEnvVar,
},
{
Expand Down
47 changes: 0 additions & 47 deletions internal/agent/agent.go

This file was deleted.

33 changes: 30 additions & 3 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,36 @@

package aws

import (
"encoding/json"
"time"
)

// Credential Convenience representation of an AWS credential.
type Credential struct {
AccessKeyID string `ini:"aws_access_key_id"`
SecretAccessKey string `ini:"aws_secret_access_key"`
SessionToken string `ini:"aws_session_token"`
AccessKeyID string `ini:"aws_access_key_id" json:"AccessKeyId,omitempty"`
SecretAccessKey string `ini:"aws_secret_access_key" json:"SecretAccessKey,omitempty"`
SessionToken string `ini:"aws_session_token" json:"SessionToken,omitempty"`
Version int `ini:"aws_version" json:"Version,omitempty"`
Expiration *time.Time `ini:"aws_expiration" json:"Expiration,omitempty"`
}

// MarshalJSON ensure Expiration date time is formatted RFC 3339 format.
func (c *Credential) MarshalJSON() ([]byte, error) {
type Alias Credential
var exp string
if c.Expiration != nil {
exp = c.Expiration.Format(time.RFC3339)
}

obj := &struct {
*Alias
Expiration string `json:"Expiration"`
}{
Alias: (*Alias)(c),
}
if exp != "" {
obj.Expiration = exp
}
return json.Marshal(obj)
}
35 changes: 35 additions & 0 deletions internal/aws/aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2023-Present, Okta, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package aws

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestCredentialJSON(t *testing.T) {
hbtGo := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
c := Credential{
Expiration: &hbtGo,
}
credStr, err := json.Marshal(c)
require.NoError(t, err)
require.Equal(t, `{"Expiration":"2009-11-10T23:00:00Z"}`, string(credStr))
}
17 changes: 16 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,31 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"

"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)

// UserAgentValue the user agent value
var UserAgentValue string

func init() {
UserAgentValue = fmt.Sprintf("okta-aws-cli/%s (%s; %s; %s)", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}

const (
// Version app version
Version = "2.0.0-beta.0"
Version = "2.0.0-beta.1"

// AWSCredentialsFormat format const
AWSCredentialsFormat = "aws-credentials"
// EnvVarFormat format const
EnvVarFormat = "env-var"
// ProcessCredentialsFormat format const
ProcessCredentialsFormat = "process-credentials"

// AuthzIDFlag cli flag const
AuthzIDFlag = "authz-id"
Expand Down Expand Up @@ -806,6 +816,11 @@ awscli:
return nil
}

// IsProcessCredentialsFormat is our format process credentials?
func (c *Config) IsProcessCredentialsFormat() bool {
return c.format == ProcessCredentialsFormat
}

type realClock struct{}

func (realClock) Now() time.Time { return time.Now() }
13 changes: 8 additions & 5 deletions internal/m2mauth/m2mauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (
AccessKeyID: *svcResp.Credentials.AccessKeyId,
SecretAccessKey: *svcResp.Credentials.SecretAccessKey,
SessionToken: *svcResp.Credentials.SessionToken,
Expiration: svcResp.Credentials.Expiration,
}

return credential, nil
}

Expand Down Expand Up @@ -199,14 +201,15 @@ func (m *M2MAuthentication) accessToken() (*okta.AccessToken, error) {
query.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
query.Add("client_assertion", clientAssertion)
tokenRequestURL += "?" + query.Encode()
tokenRequest, err := http.NewRequest("POST", tokenRequestURL, tokenRequestBuff)
req, err := http.NewRequest("POST", tokenRequestURL, tokenRequestBuff)
if err != nil {
return nil, err
}

tokenRequest.Header.Add("Accept", utils.ApplicationJSON)
tokenRequest.Header.Add(utils.ContentType, utils.ApplicationXFORM)
resp, err := m.config.HTTPClient().Do(tokenRequest)
req.Header.Add("Accept", utils.ApplicationJSON)
req.Header.Add(utils.ContentType, utils.ApplicationXFORM)
req.Header.Add(utils.UserAgentHeader, config.UserAgentValue)
req.Header.Add(utils.XOktaAWSCLIOperationHeader, utils.XOktaAWSCLIM2MOperation)
resp, err := m.config.HTTPClient().Do(req)
if err != nil {
return nil, err
}
Expand Down
6 changes: 0 additions & 6 deletions internal/output/aws_credentials_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,6 @@ func updateINI(config *ini.File, profile string, legacyVars bool, expiryVars boo
if len(comments) > 0 {
fmt.Fprintf(os.Stderr, "WARNING: Commented out %q profile keys \"%s\". Uncomment if third party tools use these values.\n", profile, strings.Join(comments, "\", \""))
}
if legacyVars {
fmt.Fprintf(os.Stderr, "WARNING: %q includes legacy variable \"aws_security_token\". Update tools making use of this deprecated value.\n", profile)
}
if expiryVars {
fmt.Fprintf(os.Stderr, "WARNING: %q includes 3rd party variable \"x_security_token_expires\".\n", profile)
}

return config, nil
}
Expand Down
2 changes: 2 additions & 0 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func RenderAWSCredential(cfg *config.Config, ac *aws.Credential) error {
case config.AWSCredentialsFormat:
expiry := time.Now().Add(time.Duration(cfg.AWSSessionDuration()) * time.Second).Format(time.RFC3339)
o = NewAWSCredentialsFile(cfg.LegacyAWSVariables(), cfg.ExpiryAWSVariables(), expiry)
case config.ProcessCredentialsFormat:
o = NewProcessCredentials()
default:
o = NewEnvVar(cfg.LegacyAWSVariables())
fmt.Fprintf(os.Stderr, "\n")
Expand Down
50 changes: 50 additions & 0 deletions internal/output/process_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2023-Present, Okta, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package output

import (
"encoding/json"
"fmt"

"github.com/okta/okta-aws-cli/internal/aws"
"github.com/okta/okta-aws-cli/internal/config"
)

// ProcessCredentials AWS CLI Process Credentials output formatter
// https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html
type ProcessCredentials struct{}

// NewProcessCredentials Creates a new ProcessCredentials
func NewProcessCredentials() *ProcessCredentials {
return &ProcessCredentials{}
}

// Output Satisfies the Outputter interface and outputs AWS credentials as JSON
// to STDOUT
func (p *ProcessCredentials) Output(c *config.Config, ac *aws.Credential) error {
// See AWS docs: "Note As of this writing, the Version key must be set to 1.
// This might increment over time as the structure evolves."
ac.Version = 1

credJSON, err := json.MarshalIndent(ac, "", " ")
if err != nil {
return err
}

fmt.Printf("%s", credJSON)
return nil
}
45 changes: 45 additions & 0 deletions internal/output/process_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2023-Present, Okta, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package output

import (
"encoding/json"
"testing"
"time"

"github.com/okta/okta-aws-cli/internal/aws"
"github.com/stretchr/testify/require"
)

func TestProcessCredentials(t *testing.T) {
credsJSON := `
{
"Version": 1,
"AccessKeyId": "an AWS access key",
"SecretAccessKey": "your AWS secret access key",
"SessionToken": "the AWS session token for temporary credentials",
"Expiration": "2009-11-10T23:00:00Z"
}`
result := aws.Credential{}
err := json.Unmarshal([]byte(credsJSON), &result)
require.NoError(t, err)
require.Equal(t, "an AWS access key", result.AccessKeyID)
require.Equal(t, "your AWS secret access key", result.SecretAccessKey)
require.Equal(t, "the AWS session token for temporary credentials", result.SessionToken)
when := time.Time(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
require.Equal(t, &when, result.Expiration)
}
Loading

0 comments on commit 1269a4b

Please sign in to comment.