Skip to content

Commit

Permalink
doc(README) Updated readme and some more testing and fixes
Browse files Browse the repository at this point in the history
* Fix for ADFS3 MFA being required, this is now optional
* Lots of small linting issues fixes
* More test coverage
  • Loading branch information
wolfeidau committed Oct 31, 2017
1 parent 357e19d commit 6763a82
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 119 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ compile: deps
lint:
gometalinter --vendor ./...

# gofmt and goimports all go files
fmt:
find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done

install:
go install ./cmd/saml2aws

Expand All @@ -64,4 +68,4 @@ clean:
rm ./glide
rm -fr ./build

.PHONY: default deps compile lint dist release test clean
.PHONY: default deps compile lint fmt dist release test clean
78 changes: 22 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ How to Implement a General Solution for Federated API/CLI Access Using SAML 2.0]

The process goes something like this:

* Setup an account alias, either using the default or given a name
* Prompt user for credentials
* Log in to Identity Provider using form based authentication
* Build a SAML assertion containing AWS roles
Expand Down Expand Up @@ -37,11 +38,17 @@ usage: saml2aws [<flags>] <command> [<args> ...]
A command line tool to help with SAML access to the AWS token service.
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
-p, --profile="saml" The AWS profile to save the temporary credentials
-s, --skip-verify Skip verification of server certificate.
-i, --provider="ADFS" The type of SAML IDP provider.
--version Show application version.
--help Show context-sensitive help (also try --help-long and --help-man).
--verbose Enable verbose logging
--version Show application version.
-a, --idp-account="default" The name of the configured IDP account
-p, --profile="saml" The AWS profile to save the temporary credentials
-s, --skip-verify Skip verification of server certificate.
--hostname=HOSTNAME The hostname of the SAML IDP server used to login.
--username=USERNAME The username used to login.
--password=PASSWORD The password used to login.
--role=ROLE The ARN of the role to assume.
--skip-prompt Skip prompting for parameters during login.
Commands:
help [<command>...]
Expand All @@ -55,16 +62,11 @@ Commands:
exec [<command>...]
Exec the supplied command with env vars from STS token.
```
saml2aws will default to using ADFS 3.x as the Identity Provider. To use another provider, use the `--provider` flag:
| IdP | |
| ------------ | ----------------------- |
| ADFS 2.x | `--provider=ADFS2` |
| PingFederate | `--provider=Ping` |
| JumpCloud | `--provider=JumpCloud` |
| Okta | `--provider=Okta` |
| KeyCloak | `--provider=KeyCloak` |
configure
Configure a new IDP account.
```

# Install

Expand Down Expand Up @@ -94,12 +96,12 @@ Install the AWS CLI see https://docs.aws.amazon.com/cli/latest/userguide/install
brew install awscli
```

Configure an empty default profile with your region of choice.
Configure an empty default profile with your region of choice, note the credentials will be overwritten when you first login and are supplied to unsure `~/.aws/credentials` file is created.

```
$ aws configure
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
AWS Access Key ID [None]: test
AWS Secret Access Key [None]: test
Default region name [None]: us-west-2
Default output format [None]:
```
Expand All @@ -112,40 +114,12 @@ Log into a service.

```
$ saml2aws login
Hostname [id.example.com]:
Username [mark.wolfe@example.com]:
Password: ************
ADFS https://id.example.com
Authenticating to ADFS...
Please choose the role you would like to assume:
[ 0 ]: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSBuild
[ 1 ]: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
Selection: 1
Selected role: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
Requesting AWS credentials using SAML assertion
Saving credentials
Logged in as: arn:aws:sts::123123123123:assumed-role/AWS-Admin-CloudOPSNonProd/wolfeidau@example.com
Your new access key pair has been stored in the AWS configuration
Note that it will expire at 2016-09-19 15:59:49 +1000 AEST
To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile saml ec2 describe-instances).
```

Run ansible with an expired token present, `exec` verifies the token and requests login.

```
$ saml2aws exec --skip-verify -- ansible-playbook -e "aws_region=ap-southeast-2" playbook.yml
Hostname [id.example.com]:
Using IDP Account default to access Ping https://id.example.com
To use saved password just hit enter.
Username [mark.wolfe@example.com]:
Password: ************
ADFS https://id.example.com
Authenticating to ADFS...
Please choose the role you would like to assume:
[ 0 ]: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSBuild
[ 1 ]: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
Selection: 1
Authenticating as mark.wolfe@example.com ...
Selected role: arn:aws:iam::123123123123:role/AWS-Admin-CloudOPSNonProd
Requesting AWS credentials using SAML assertion
Saving credentials
Expand All @@ -154,14 +128,6 @@ Logged in as: arn:aws:sts::123123123123:assumed-role/AWS-Admin-CloudOPSNonProd/w
Your new access key pair has been stored in the AWS configuration
Note that it will expire at 2016-09-19 15:59:49 +1000 AEST
To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile saml ec2 describe-instances).
PLAY [create cloudformation stack] *************************************************
...
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
```

# Building
Expand Down
10 changes: 5 additions & 5 deletions aws_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ func TestExtractAWSAccounts(t *testing.T) {

func TestAssignPrincipals(t *testing.T) {
awsRoles := []*AWSRole{
&AWSRole{
{
PrincipalARN: "arn:aws:iam::000000000001:saml-provider/test-idp",
RoleARN: "arn:aws:iam::000000000001:role/Development",
},
}

awsAccounts := []*AWSAccount{
&AWSAccount{
{
Roles: []*AWSRole{
&AWSRole{
{
RoleARN: "arn:aws:iam::000000000001:role/Development",
},
},
Expand All @@ -60,11 +60,11 @@ func TestAssignPrincipals(t *testing.T) {

func TestLocateRole(t *testing.T) {
awsRoles := []*AWSRole{
&AWSRole{
{
PrincipalARN: "arn:aws:iam::000000000001:saml-provider/test-idp",
RoleARN: "arn:aws:iam::000000000001:role/Development",
},
&AWSRole{
{
PrincipalARN: "arn:aws:iam::000000000002:saml-provider/test-idp",
RoleARN: "arn:aws:iam::000000000002:role/Development",
},
Expand Down
132 changes: 80 additions & 52 deletions cmd/saml2aws/commands/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
"github.com/versent/saml2aws/pkg/creds"
)

// MaxDurationSeconds the maximum duration in seconds for an STS session
const MaxDurationSeconds = 3600

// LoginFlags login specific command flags
type LoginFlags struct {
IdpAccount string
Expand All @@ -38,43 +41,27 @@ func (lf *LoginFlags) RoleSupplied() bool {
// Login login to ADFS
func Login(loginFlags *LoginFlags) error {

cfgm, err := cfg.NewConfigManager(cfg.DefaultConfigPath)
account, err := buildIdpAccount(loginFlags)
if err != nil {
return errors.Wrap(err, "failed to load configuration")
return errors.Wrap(err, "error building login details")
}

account, err := cfgm.LoadVerifyIDPAccount(loginFlags.IdpAccount)
if err != nil {
if cfg.IsErrIdpAccountNotFound(err) {
fmt.Printf("%v\n", err)
os.Exit(1)
}
return errors.Wrap(err, "failed to load idp account")
}

// update username and hostname if supplied
applyFlagOverrides(loginFlags, account)

loginDetails := &creds.LoginDetails{Hostname: account.Hostname, Username: account.Username}

fmt.Printf("Using IDP Account %s to access %s https://%s\n", loginFlags.IdpAccount, account.Provider, account.Hostname)

err = resolveLoginDetails(loginDetails, loginFlags)
loginDetails, err := resolveLoginDetails(account, loginFlags)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}

fmt.Printf("Authenticating as %s ...\n", account.Username)

provider, err := saml2aws.NewSAMLClient(account)
err = loginDetails.Validate()
if err != nil {
return errors.Wrap(err, "error building IdP client")
return errors.Wrap(err, "error validating login details")
}

err = loginDetails.Validate()
provider, err := saml2aws.NewSAMLClient(account)
if err != nil {
return errors.Wrap(err, "error validating login details")
return errors.Wrap(err, "error building IdP client")
}

samlAssertion, err := provider.Authenticate(loginDetails)
Expand Down Expand Up @@ -122,53 +109,52 @@ func Login(loginFlags *LoginFlags) error {

fmt.Println("Selected role:", role.RoleARN)

sess, err := session.NewSession()
err = loginToStsUsingRole(role, samlAssertion, loginFlags.Profile)
if err != nil {
return errors.Wrap(err, "failed to create session")
return errors.Wrap(err, "error logging into aws role using saml assertion")
}

svc := sts.New(sess)
return nil
}

params := &sts.AssumeRoleWithSAMLInput{
PrincipalArn: aws.String(role.PrincipalARN), // Required
RoleArn: aws.String(role.RoleARN), // Required
SAMLAssertion: aws.String(samlAssertion), // Required
DurationSeconds: aws.Int64(3600), // 1 hour
func buildIdpAccount(loginFlags *LoginFlags) (*cfg.IDPAccount, error) {
cfgm, err := cfg.NewConfigManager(cfg.DefaultConfigPath)
if err != nil {
return nil, errors.Wrap(err, "failed to load configuration")
}

fmt.Println("Requesting AWS credentials using SAML assertion")

resp, err := svc.AssumeRoleWithSAML(params)
account, err := cfgm.LoadVerifyIDPAccount(loginFlags.IdpAccount)
if err != nil {
return errors.Wrap(err, "error retrieving STS credentials using SAML")
if cfg.IsErrIdpAccountNotFound(err) {
fmt.Printf("%v\n", err)
os.Exit(1)
}
return nil, errors.Wrap(err, "failed to load idp account")
}

// fmt.Println("Saving credentials")

sharedCreds := awsconfig.NewSharedCredentials(loginFlags.Profile)
// update username and hostname if supplied
applyFlagOverrides(loginFlags, account)

err = sharedCreds.Save(aws.StringValue(resp.Credentials.AccessKeyId), aws.StringValue(resp.Credentials.SecretAccessKey), aws.StringValue(resp.Credentials.SessionToken))
err = account.Validate()
if err != nil {
return errors.Wrap(err, "error saving credentials")
return nil, errors.Wrap(err, "failed to validate account")
}

fmt.Println("Logged in as:", aws.StringValue(resp.AssumedRoleUser.Arn))
fmt.Println("")
fmt.Println("Your new access key pair has been stored in the AWS configuration")
fmt.Printf("Note that it will expire at %v\n", resp.Credentials.Expiration.Local())
fmt.Println("To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile", loginFlags.Profile, "ec2 describe-instances).")

return nil
return account, nil
}

func resolveLoginDetails(loginDetails *creds.LoginDetails, loginFlags *LoginFlags) error {
func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *LoginFlags) (*creds.LoginDetails, error) {

// fmt.Printf("loginFlags %+v\n", loginFlags)

loginDetails := &creds.LoginDetails{Hostname: account.Hostname, Username: account.Username}

fmt.Printf("Using IDP Account %s to access %s https://%s\n", loginFlags.IdpAccount, account.Provider, account.Hostname)

err := credentials.LookupCredentials(loginDetails)
if err != nil {
if !credentials.IsErrCredentialsNotFound(err) {
return errors.Wrap(err, "error loading saved password")
return nil, errors.Wrap(err, "error loading saved password")
}
}

Expand All @@ -188,14 +174,15 @@ func resolveLoginDetails(loginDetails *creds.LoginDetails, loginFlags *LoginFlag

// if skip prompt was passed just pass back the flag values
if loginFlags.SkipPrompt {
return nil
return loginDetails, nil
}

err = saml2aws.PromptForLoginDetails(loginDetails)
if err != nil {
return errors.Wrap(err, "Error occured accepting input")
return nil, errors.Wrap(err, "Error occurred accepting input")
}
return nil

return loginDetails, nil
}

func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags *LoginFlags) (*saml2aws.AWSRole, error) {
Expand Down Expand Up @@ -232,6 +219,47 @@ func resolveRole(awsRoles []*saml2aws.AWSRole, samlAssertion string, loginFlags
return role, nil
}

func loginToStsUsingRole(role *saml2aws.AWSRole, samlAssertion string, profile string) error {

sess, err := session.NewSession()
if err != nil {
return errors.Wrap(err, "failed to create session")
}

svc := sts.New(sess)

params := &sts.AssumeRoleWithSAMLInput{
PrincipalArn: aws.String(role.PrincipalARN), // Required
RoleArn: aws.String(role.RoleARN), // Required
SAMLAssertion: aws.String(samlAssertion), // Required
DurationSeconds: aws.Int64(MaxDurationSeconds), // 1 hour
}

fmt.Println("Requesting AWS credentials using SAML assertion")

resp, err := svc.AssumeRoleWithSAML(params)
if err != nil {
return errors.Wrap(err, "error retrieving STS credentials using SAML")
}

// fmt.Println("Saving credentials")

sharedCreds := awsconfig.NewSharedCredentials(profile)

err = sharedCreds.Save(aws.StringValue(resp.Credentials.AccessKeyId), aws.StringValue(resp.Credentials.SecretAccessKey), aws.StringValue(resp.Credentials.SessionToken))
if err != nil {
return errors.Wrap(err, "error saving credentials")
}

fmt.Println("Logged in as:", aws.StringValue(resp.AssumedRoleUser.Arn))
fmt.Println("")
fmt.Println("Your new access key pair has been stored in the AWS configuration")
fmt.Printf("Note that it will expire at %v\n", resp.Credentials.Expiration.Local())
fmt.Println("To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile", profile, "ec2 describe-instances).")

return nil
}

func applyFlagOverrides(loginFlags *LoginFlags, account *cfg.IDPAccount) {
if loginFlags.Hostname != "" {
account.Hostname = loginFlags.Hostname
Expand Down
Loading

0 comments on commit 6763a82

Please sign in to comment.