Skip to content

Commit

Permalink
Merge pull request #146 from okta/m2m_collect_all_idps_and_roles
Browse files Browse the repository at this point in the history
web command - collect all roles for an AWS Fed App (idp) at once
  • Loading branch information
monde authored Oct 11, 2023
2 parents 47a50b2 + 914ae1b commit 2915994
Show file tree
Hide file tree
Showing 15 changed files with 366 additions and 92 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ $ okta-aws-cli web \
--exec -- aws ec2 describe-instances
```

### (Complete) Collect all roles for an AWS Fed App (IdP) at once

`okta-aws-cli web` will collect all available AWS IAM Roles for a given Okta AWS
Federation app (IdP) at once. This is a feature specific to writing the
`$HOME/.aws/credentials` file. Roles will be AWS account alias name (if STS list
aliases is available on the given role) then `-` then abbreviated role name.


```
# AWS account alias "myorg", given IdP associated with "AWS Account Federation"
# and an app associated with two roles.
$ okta-aws-cli web \
--org-domain test.okta.com \
--oidc-client-id 0oa5wyqjk6Wm148fE1d7 \
--write-aws-credentials \
--all-profiles
? Choose an IdP: AWS Account Federation
Updated profile "myorg-S3-read" in credentials file "/Users/me/.aws/credentials".
Updated profile "myorg-S3-write" in credentials file "/Users/me/.aws/credentials".
```

### (expected) Alternate web browser open command

The `web` command will open the system's default web browser when the
Expand All @@ -71,6 +94,11 @@ $ okta-aws-cli web \
--open-browser-command "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --profile-directory='Profile 1'"
```

## 2.0.0-beta.3 (October 10, 2023)

`okta-aws-cli web` can collect all roles to an AWS credentials file for a given
AWS Federation App (IdP) in one invocation of the CLI.

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

Execute a subcommand directly from `okta-aws-cli`
Expand Down
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ documentation](#configuration).*

`okta-aws-cli` is a CLI program allowing Okta to act as an identity provider and
retrieve AWS IAM temporary credentials for use in AWS CLI, AWS SDKs, and other
tools accessing the AWS API. There are two primary commands of operation: `web` -
combined human and device authorization; and `m2m` - headless authorization.
tools accessing the AWS API. There are two primary commands of operation: `web`
- combined human and device authorization; and `m2m` - headless authorization.
`okta-aws-cli web` is native to the Okta Identity Engine and its authentication
and device authorization flows. `okta-aws-cli web` is not compatible with Okta
Classic orgs. `okta-aws-cli m2m` makes use of public/private authorization and
OIDC.
Classic orgs. `okta-aws-cli m2m` makes use of private key (OAuth2)
authorization and OIDC.

Example `okta-aws-cli` `web` command with environment variables (when command is
missing *defaults* to `web`) output:
Expand Down Expand Up @@ -390,6 +390,7 @@ These settings are all optional:
| AWS IAM Identity Provider ARN | Preselects the IdP list to this preferred IAM Identity Provider. If there are other IdPs available they will not be listed. | `--aws-iam-idp [value]` | `OKTA_AWSCLI_IAM_IDP` |
| Display QR Code | `true` if flag is present | `--qr-code` | `OKTA_AWSCLI_QR_CODE=true` |
| Automatically open the activation URL with the system web browser | `true` if flag is present | `--open-browser` | `OKTA_AWSCLI_OPEN_BROWSER=true` |
| Gather all profiles for a given IdP (implies aws-credentials file output format)) | `true` if flag is present | `--all-profiles` | `OKTA_AWSCLI_OPEN_BROWSER=true` |

#### Allowed Web SSO Client ID

Expand Down Expand Up @@ -736,6 +737,28 @@ make_bucket failed: s3://no-access-example An error occurred (AccessDenied) when
Error: exit status 1
```

### Collect all roles for an AWS Fed App (IdP) at once

`okta-aws-cli web` will collect all available AWS IAM Roles for a given Okta AWS
Federation app (IdP) at once. This is a feature specific to writing the
`$HOME/.aws/credentials` file. Roles will be AWS account alias name (if STS list
aliases is available on the given role) then `-` then abbreviated role name.

```
# AWS account alias "myorg", given IdP associated with "AWS Account Federation"
# and an app associated with two roles.
$ okta-aws-cli web \
--org-domain test.okta.com \
--oidc-client-id 0oa5wyqjk6Wm148fE1d7 \
--write-aws-credentials \
--all-profiles
? Choose an IdP: AWS Account Federation
Updated profile "myorg-S3-read" in credentials file "/Users/me/.aws/credentials".
Updated profile "myorg-S3-write" in credentials file "/Users/me/.aws/credentials".
```

### Help

```shell
Expand Down
7 changes: 7 additions & 0 deletions cmd/root/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ var (
Usage: "Automatically open the activation URL with the system web browser",
EnvVar: config.OpenBrowserEnvVar,
},
{
Name: config.AllProfilesFlag,
Short: "k",
Value: false,
Usage: "Collect all profiles for a given IdP (implies aws-credentials file output format)",
EnvVar: config.AllProfilesEnvVar,
},
}
requiredFlags = []string{"org-domain", "oidc-client-id"}
)
Expand Down
71 changes: 62 additions & 9 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,67 @@ import (
"time"
)

// Credential Convenience representation of an AWS credential.
type Credential struct {
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"`
// Credential Interface to represent AWS credentials in different formats.
type Credential interface {
// Trivial function to allow concrete structs to be represented by this
// interface.
IsCredential() bool
}

// ProcessCredential Convenience representation of an AWS credential used for process credential format.
// CredentialContainer denormalized struct of all the values can be presented in
// the different credentials formats
type CredentialContainer struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
Expiration *time.Time
Version int
Profile string
}

// EnvVarCredential representation of an AWS credential for environment
// variables
type EnvVarCredential struct {
AccessKeyID string
SecretAccessKey string
SessionToken string
}

// IsCredential env var credential is a credential
func (e *EnvVarCredential) IsCredential() bool { return true }

// CredsFileCredential representation of an AWS credential for the AWS
// credentials file
type CredsFileCredential struct {
AccessKeyID string `ini:"aws_access_key_id"`
SecretAccessKey string `ini:"aws_secret_access_key"`
SessionToken string `ini:"aws_session_token"`

profile string
}

// IsCredential creds file credential is a credential
func (c *CredsFileCredential) IsCredential() bool { return true }

// SetProfile sets the profile name associated with this AWS credential.
func (c *CredsFileCredential) SetProfile(p string) { c.profile = p }

// Profile returns the profile name associated with this AWS credential.
func (c CredsFileCredential) Profile() string { return c.profile }

// ProcessCredential Convenience representation of an AWS credential used for
// process credential format.
type ProcessCredential struct {
Credential
Version int `json:"Version,omitempty"`
Expiration *time.Time `json:"Expiration,omitempty"`
AccessKeyID string `json:"AccessKeyId,omitempty"`
SecretAccessKey string `json:"SecretAccessKey,omitempty"`
SessionToken string `json:"SessionToken,omitempty"`
Expiration *time.Time `json:"Expiration,omitempty"`
Version int `json:"Version,omitempty"`
}

// IsCredential process credential is a credential
func (c *ProcessCredential) IsCredential() bool { return true }

// MarshalJSON ensure Expiration date time is formatted RFC 3339 format.
func (c *ProcessCredential) MarshalJSON() ([]byte, error) {
type Alias ProcessCredential
Expand All @@ -54,3 +101,9 @@ func (c *ProcessCredential) MarshalJSON() ([]byte, error) {
}
return json.Marshal(obj)
}

// NoopCredential Convenience representation for not printing credentials
type NoopCredential struct{}

// IsCredential noop credential is a credential
func (n *NoopCredential) IsCredential() bool { return true }
42 changes: 42 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const (
// NoopFormat format const
NoopFormat = "noop"

// AllProfilesFlag cli flag const
AllProfilesFlag = "all-profiles"
// AuthzIDFlag cli flag const
AuthzIDFlag = "authz-id"
// AWSAcctFedAppIDFlag cli flag const
Expand Down Expand Up @@ -95,6 +97,8 @@ const (
// CacheAccessTokenFlag cli flag const
CacheAccessTokenFlag = "cache-access-token"

// AllProfilesEnvVar env var const
AllProfilesEnvVar = "OKTA_AWSCLI_ALL_PROFILES"
// AuthzIDEnvVar env var const
AuthzIDEnvVar = "OKTA_AWSCLI_AUTHZ_ID"
// AWSCredentialsEnvVar env var const
Expand Down Expand Up @@ -123,10 +127,16 @@ const (
LegacyAWSVariablesEnvVar = "OKTA_AWSCLI_LEGACY_AWS_VARIABLES"
// OktaOIDCClientIDEnvVar env var const
OktaOIDCClientIDEnvVar = "OKTA_AWSCLI_OIDC_CLIENT_ID"
// OldOktaOIDCClientIDEnvVar env var const
OldOktaOIDCClientIDEnvVar = "OKTA_OIDC_CLIENT_ID"
// OktaOrgDomainEnvVar env var const
OktaOrgDomainEnvVar = "OKTA_AWSCLI_ORG_DOMAIN"
// OldOktaOrgDomainEnvVar env var const
OldOktaOrgDomainEnvVar = "OKTA_ORG_DOMAIN"
// OktaAWSAccountFederationAppIDEnvVar env var const
OktaAWSAccountFederationAppIDEnvVar = "OKTA_AWSCLI_AWS_ACCOUNT_FEDERATION_APP_ID"
// OldOktaAWSAccountFederationAppIDEnvVar env var const
OldOktaAWSAccountFederationAppIDEnvVar = "OKTA_AWS_ACCOUNT_FEDERATION_APP_ID"
// OpenBrowserEnvVar env var const
OpenBrowserEnvVar = "OKTA_AWSCLI_OPEN_BROWSER"
// PrivateKeyEnvVar env var const
Expand Down Expand Up @@ -171,6 +181,7 @@ type Clock interface {
// control data access, be concerned with evaluation, validation, and not
// allowing direct access to values as is done on structs in the generic case.
type Config struct {
allProfiles bool
authzID string
awsCredentials string
awsIAMIdP string
Expand Down Expand Up @@ -199,6 +210,7 @@ type Config struct {

// Attributes config construction
type Attributes struct {
AllProfiles bool
AuthzID string
AWSCredentials string
AWSIAMIdP string
Expand Down Expand Up @@ -239,6 +251,7 @@ func EvaluateSettings() (*Config, error) {
func NewConfig(attrs *Attributes) (*Config, error) {
var err error
cfg := &Config{
allProfiles: attrs.AllProfiles,
authzID: attrs.AuthzID,
awsCredentials: attrs.AWSCredentials,
awsIAMIdP: attrs.AWSIAMIdP,
Expand Down Expand Up @@ -285,6 +298,7 @@ func NewConfig(attrs *Attributes) (*Config, error) {

func readConfig() (Attributes, error) {
attrs := Attributes{
AllProfiles: viper.GetBool(AllProfilesFlag),
AuthzID: viper.GetString(AuthzIDFlag),
AWSCredentials: viper.GetString(AWSCredentialsFlag),
AWSIAMIdP: viper.GetString(AWSIAMIdPFlag),
Expand Down Expand Up @@ -326,12 +340,22 @@ func readConfig() (Attributes, error) {
if attrs.OrgDomain == "" {
attrs.OrgDomain = viper.GetString(downCase(OktaOrgDomainEnvVar))
}
if attrs.OrgDomain == "" {
// legacy support OKTA_ORG_DOMAIN
attrs.OrgDomain = viper.GetString(downCase(OldOktaOrgDomainEnvVar))
}
if attrs.OIDCAppID == "" {
attrs.OIDCAppID = viper.GetString(downCase(OktaOIDCClientIDEnvVar))
}
if attrs.OIDCAppID == "" {
attrs.OIDCAppID = viper.GetString(downCase(OldOktaOIDCClientIDEnvVar))
}
if attrs.FedAppID == "" {
attrs.FedAppID = viper.GetString(downCase(OktaAWSAccountFederationAppIDEnvVar))
}
if attrs.FedAppID == "" {
attrs.FedAppID = viper.GetString(downCase(OldOktaAWSAccountFederationAppIDEnvVar))
}
if attrs.AWSIAMIdP == "" {
attrs.AWSIAMIdP = viper.GetString(downCase(AWSIAMIdPEnvVar))
}
Expand All @@ -353,6 +377,9 @@ func readConfig() (Attributes, error) {
if attrs.AuthzID == "" {
attrs.AuthzID = viper.GetString(downCase(AuthzIDEnvVar))
}
if !attrs.AllProfiles {
attrs.AllProfiles = viper.GetBool(downCase(AllProfilesEnvVar))
}

// if session duration is 0, inspect the ENV VAR for a value, else set
// a default of 3600
Expand Down Expand Up @@ -399,6 +426,10 @@ func readConfig() (Attributes, error) {
// writing aws creds option implies "aws-credentials" format
attrs.Format = AWSCredentialsFormat
}
if attrs.AllProfiles {
// writing all aws profiles option implies "aws-credentials" format
attrs.Format = AWSCredentialsFormat
}
if !attrs.OpenBrowser {
attrs.OpenBrowser = viper.GetBool(downCase(OpenBrowserEnvVar))
}
Expand Down Expand Up @@ -428,6 +459,17 @@ func downCase(s string) string {
return strings.ToLower(s)
}

// AllProfiles --
func (c *Config) AllProfiles() bool {
return c.allProfiles
}

// SetAllProfiles --
func (c *Config) SetAllProfiles(allProfiles bool) error {
c.allProfiles = allProfiles
return nil
}

// AuthzID --
func (c *Config) AuthzID() string {
return c.authzID
Expand Down
8 changes: 4 additions & 4 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func NewExec() (*Exec, error) {
}

// Run Run the executor
func (e *Exec) Run(oc *oaws.Credential) error {
func (e *Exec) Run(cc *oaws.CredentialContainer) error {
pairs := map[string]string{}
// pre-populate pairs with any existing env var starting with "AWS_"
for _, kv := range os.Environ() {
Expand All @@ -74,9 +74,9 @@ func (e *Exec) Run(oc *oaws.Credential) error {
}
}
// add creds env var names to pairs
pairs["AWS_ACCESS_KEY_ID"] = oc.AccessKeyID
pairs["AWS_SECRET_ACCESS_KEY"] = oc.SecretAccessKey
pairs["AWS_SESSION_TOKEN"] = oc.SessionToken
pairs["AWS_ACCESS_KEY_ID"] = cc.AccessKeyID
pairs["AWS_SECRET_ACCESS_KEY"] = cc.SecretAccessKey
pairs["AWS_SESSION_TOKEN"] = cc.SessionToken

cmd := osexec.Command(e.name, e.args...)
for k, v := range pairs {
Expand Down
13 changes: 7 additions & 6 deletions internal/m2mauth/m2mauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,27 @@ func (m *M2MAuthentication) EstablishIAMCredentials() error {
return err
}

oc, ac, err := m.awsAssumeRoleWithWebIdentity(at)
cc, err := m.awsAssumeRoleWithWebIdentity(at)
if err != nil {
return err
}

err = output.RenderAWSCredential(m.config, oc, ac)
err = output.RenderAWSCredential(m.config, cc)
if err != nil {
return err
}

if m.config.Exec() {
exe, _ := exec.NewExec()
if err := exe.Run(oc); err != nil {
if err := exe.Run(cc); err != nil {
return err
}
}

return nil
}

func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (oc *oaws.Credential, ac *sts.Credentials, err error) {
func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (cc *oaws.CredentialContainer, err error) {
awsCfg := aws.NewConfig().WithHTTPClient(m.config.HTTPClient())
sess, err := session.NewSession(awsCfg)
if err != nil {
Expand All @@ -130,13 +130,14 @@ func (m *M2MAuthentication) awsAssumeRoleWithWebIdentity(at *okta.AccessToken) (
return
}

oc = &oaws.Credential{
cc = &oaws.CredentialContainer{
AccessKeyID: *svcResp.Credentials.AccessKeyId,
SecretAccessKey: *svcResp.Credentials.SecretAccessKey,
SessionToken: *svcResp.Credentials.SessionToken,
Expiration: svcResp.Credentials.Expiration,
}

return oc, svcResp.Credentials, nil
return cc, nil
}

func (m *M2MAuthentication) createKeySigner() (jose.Signer, error) {
Expand Down
Loading

0 comments on commit 2915994

Please sign in to comment.