Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike Vanbuskirk committed May 27, 2021
0 parents commit 8c8c134
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
29 changes: 29 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
project_name: aws-mfa
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Mike Vanbuskirk

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# aws-mfa

aws-mfa is a simple CLI application written in Golang for handling AWS CLI MFA authentication.

## How it works

The application uses the [AWS STS API](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) and your local credentials file to request temporary AWS credentials, authenticated via Multifactor Authentication(MFA).

## Prerequisites

It is assumed, at a minimum, you have access to an AWS account. You will also need:

- An IAM user or assumable role with permissions to use the STS service.
- An MFA device configured for your respective identity, capable of generating TOTP tokens.
- Configuration and credentials files in the standard locations(typically `$HOME/.aws/config` and `$HOME/.aws/credentials` respectively), with at least one account profile.

Although it's not technically "required", installing the [AWS CLI](https://aws.amazon.com/cli/) is highly recommended, as it is the most common way to create the required configuration directory and files, and ultimately provides the quickest way to validate that your credentials are valid.

## Quickstart

1. Download the latest release from the [releases page](https://github.com/codevbus/aws-mfa/releases).
2. Unzip the application to somewhere in your path, such as `/usr/local/bin`
3. Run `aws-mfa` and follow the instructions.
4. Test authentication by running `aws sts get-caller-identity`

## Credentials File Format

aws-mfa makes some assumptions about how your `$HOME/.aws/credentials` file is formatted, specifically around profile names. The easiest method is to name each profile something short and recognizable. An example credentials file for two separate AWS accounts, `dev` and `prod`:

``` ini
[dev]
aws_access_key_id = <your_access_key>
aws_secret_access_key = <your_secret_key>

[prod]
aws_access_key_id = <your_access_key>
aws_secret_access_key = <your_secret_key>
```

Note that there is *not* a profile named `default`.

If your current configuration includes a "default" profile, make a backup of your `$HOME/.aws` directory, and then rename the profile as described above. The next section explains the reasoning behind this.

## Ephemeral Credentials

The `aws` cli command, as well as most CLI utilities and applications that make calls to AWS and the AWS API are generally configured to expect AWS credentials in a few standard file locations, or as environment variables.

In the case of `aws-mfa`, we depend on the file method. In a credentials file, any profile named `default` will take precedence when making calls to the API. Once authentication completes successfull, `aws-mfa` dynamically sets a new `default` profile in `$HOME/.aws/credentials` containing temporary credentials.

Using the example file from above, here is what it would look like after a successful MFA authentication:

``` ini
[dev]
aws_access_key_id = <your_access_key>
aws_secret_access_key = <your_secret_key>

[prod]
aws_access_key_id = <your_access_key>
aws_secret_access_key = <your_secret_key>

[default]
aws_access_key_id = <your_temporary_access_key>
aws_secret_access_key = <your_temporary_secret_key>
aws_session_token = <your_temporary_session_token>
```

Note the extra key, `aws_session_token`, in the new `default` section.
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module aws-mfa

go 1.16

require (
github.com/aws/aws-sdk-go v1.38.32
github.com/smartystreets/goconvey v1.6.4 // indirect
gopkg.in/ini.v1 v1.62.0
)
37 changes: 37 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
github.com/aws/aws-sdk-go v1.38.32 h1:oA8gAVtpBNXhD91Xsx0IdRF+S7ghHaMhYdff2JiO+lk=
github.com/aws/aws-sdk-go v1.38.32/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
212 changes: 212 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package main

import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"gopkg.in/ini.v1"
)

// LoadIni will load a filename as an INI file, as well as
// a slice of the config sections
func LoadIni(filename string) (*ini.File, []*ini.Section, error) {
iniFile, err := ini.Load(filename)
if err != nil {
return nil, nil, err
}
sections := iniFile.Sections()
return iniFile, sections, nil
}

// SetCreds will populate the aws credentials file(passed as INI)
// with a new 'default' section and updated STS credentials
func SetCreds(filename string, creds *sts.Credentials, f *ini.File) {
section, err := f.NewSection("default")
if err != nil {
fmt.Println(err)
}

// Populate the section with the temp creds
fmt.Println("Setting temporary credentials in your file")
section.NewKey("aws_access_key_id", *creds.AccessKeyId)
section.NewKey("aws_secret_access_key", *creds.SecretAccessKey)
section.NewKey("aws_session_token", *creds.SessionToken)

f.SaveTo(filename)
}

// Authenticate with the AWS CLI
func Authenticate(profile, mfa, token, credfile, conffile string, s *sts.STS, conf, cred *ini.File) {
activeProf := conf.Section(profile)
// This handles authentication when a credential section is role-based instead of user-based
if activeProf.HasKey("role_arn") {
roleArn, _ := activeProf.GetKey("role_arn")
res, err := s.GetSessionToken(&sts.GetSessionTokenInput{
TokenCode: aws.String(token),
SerialNumber: aws.String(mfa),
})

if err != nil {
log.Fatalln(err)
}

id := *res.Credentials.AccessKeyId
secret := *res.Credentials.SecretAccessKey
stoken := *res.Credentials.SessionToken

sessConf := &aws.Config{
Credentials: credentials.NewStaticCredentials(id, secret, stoken),
}

sess, err := session.NewSession(sessConf)
if err != nil {
fmt.Println(err)
}

svc := sts.New(sess)
input := &sts.AssumeRoleInput{
RoleArn: aws.String(roleArn.String()),
DurationSeconds: aws.Int64(3600),
RoleSessionName: aws.String(profile + "-mfa-session"),
}

result, err := svc.AssumeRole(input)
if err != nil {
fmt.Println(err)
}

SetCreds(credfile, result.Credentials, cred)

fmt.Println("Complete!")

} else {
result, err := s.GetSessionToken(&sts.GetSessionTokenInput{
TokenCode: aws.String(token),
SerialNumber: aws.String(mfa),
})

if err != nil {
log.Fatalln(err)
}

SetCreds(credfile, result.Credentials, cred)

fmt.Println("Complete!")

}
}

func main() {
// Load the config and credential filenames
credfile := defaults.SharedCredentialsFilename()
configfile := defaults.SharedConfigFilename()

// Clear any existing env vars to avoid issues
awsvars := []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_KEY_ID", "AWS_SESSION_TOKEN"}

fmt.Println("Resetting AWS Environment variables...")
fmt.Println("===================================")
for _, v := range awsvars {
err := os.Setenv(v, "")
if err != nil {
fmt.Println(err)
}
}

// Load the credentials and config files as ini files
awscreds, credprofiles, err := LoadIni(credfile)
awsconfig, _, err := LoadIni(configfile)

var profiles []string

for _, s := range credprofiles {
if !(strings.Contains(strings.ToLower(s.Name()), "default")) {
profiles = append(profiles, s.Name())
}
}

fmt.Println("Choose which AWS profile to authenticate with:")
for i, p := range profiles {
fmt.Printf("%d: %s\n", i+1, p)
}

chooseProfile := func() string {
var profile string
for {
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
text = strings.Replace(text, "\n", "", -1)

i, err := strconv.Atoi(text)
if err != nil {
fmt.Println(err)
continue
}

if i >= 1 && i <= len(profiles) {
profile = profiles[i-1]
break
}
fmt.Println("Please choose one of the available profiles")
continue
}
return profile
}

profile := chooseProfile()

// Read in hard-coded config/credentials(needed for STS handoff)
conf := &aws.Config{
Credentials: credentials.NewSharedCredentials(credfile, profile),
}

// Create new session
sess, err := session.NewSession(conf)

// Create session with STS service for getting the initial caller identity
// and the eventual auth token
_sts := sts.New(sess)

// Get ARN of profile user
arn, err := _sts.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
fmt.Println(err)
}

// Create session with IAM service to get MFA device for chosen profile
_iam := iam.New(sess)

mfas, err := _iam.ListVirtualMFADevices(&iam.ListVirtualMFADevicesInput{
AssignmentStatus: aws.String("Assigned"),
})

var mfa string

for _, device := range mfas.VirtualMFADevices {
if *device.User.Arn == *arn.Arn {
mfa = *device.SerialNumber
}
}

// Get MFA token from user
token, err := stscreds.StdinTokenProvider()

if err != nil {
fmt.Println(err)
}

// Perform authentication
Authenticate(profile, mfa, token, credfile, configfile, _sts, awsconfig, awscreds)
}

0 comments on commit 8c8c134

Please sign in to comment.