All about Cloud, mostly about Amazon Web Services (AWS)

Quick AWS Console Login

 2018-11-09 /  1324 words /  7 minutes

I often use the AWS Management Console and in accordance with best practices I have enabled Multi-Factor Authentication (MFA) in my accounts. Starting the AWS Console first involves bringing up a browser, going to the URL, typing in my username and password. I then dig out my phone, stare at it while FaceId scans me, open the Duo app, and select the account to display the code. I then switch back to the computer and type the code into the browser. Phew! Wouldn’t it be nice if you could get a quick AWS console login using the access key and secret access key and bypassing MFA? This post shows how that can be achieved!

NOTE: MFA can also be used in two ways within AWS. First, the AWS Console can be protected by MFA. Second, IAM policies can require MFA. The approach shown below doesn’t bypass MFA if it is required by an IAM policy, it will only bypass MFA for AWS Console access.

Quick AWS Console Login

AWS provide a mechanism that allows Enterprises to build their own authentication mechanism which allows console logins. Leveraging this mechanism allows automatic login to the AWS console login without enforcing the MFA requirements.

I’ve been learning Golang (or just Go) and decided to write the program in Go. The first few lines setup the program with all the packages it needs (line 3), and declares some constants (on line 18):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "os/user"
    "strings"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/sts"
)

const consoleURL = "https://console.aws.amazon.com/"
const signInURL = "https://signin.aws.amazon.com/federation"
const policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"*\",\"Resource\":\"*\"}]}"

Next, we declare a struct to store the results of parsing the JSON response from one of the AWS web services. See way down on line 122.

22
23
24
type SigninTokenResponse struct {
    SigninToken string `json:"SigninToken"`
}

We then (on line 26) declare the main entry point for the program, declare some variables we’ll use later (line 28), declare our command line parameters (using the “flag” package) and then parse them (on line 35):

26
27
28
29
30
31
32
33
34
35
func main() {

    var profile string
    var accessKeyId string
    var secretAccessKey string
    var sessionToken string

    flag.StringVar(&profile, "profile", "", "Process a specific service - specify service name")
    flag.StringVar(&profile, "p", "", "Process a specific service - specify service name")
    flag.Parse()

In order to build a session name, we read the the username of the current logged in user (on line 37), and generate an error if there are any problems:

37
38
39
40
	cuser, err := user.Current()
	if err != nil {
		panic(fmt.Sprintf("Error determining current user: %s", err))
	}

Establishing Credentials

We then attempt to establish a session with AWS. First we look to see if the user has used “-p” or “–profile” to configure a specific profile to use. AWS to allow users to switch between multiple accounts and regions using different profiles. If not specified, we create a default session (line 44), otherwise the specified profile is used to create the session (line 46-48).

42
43
44
45
46
47
48
49
50
    var sess *session.Session
    if len(profile) == 0 {
        sess = session.New()
    } else {
        sess = session.Must(session.NewSessionWithOptions(session.Options{
            SharedConfigState: session.SharedConfigEnable,
            Profile: profile,
        }))
    }

Next we use the Security Token Service (STS) to generate a Federation Token with a specific duration, the AWS IAM policy intersection, and the session name (on line 54). We set the session duration to 1 hour. Next set the session name to the name of the current user. We also set the policy intersection to the “anything goes” policy (line 20). This means that the created session will have all the same access as the access key and secret access key. Check for errors then assign the new access key, secret access key and session token to the variables declared above.

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
	svc := sts.New(sess)

	input := &sts.GetFederationTokenInput{
		DurationSeconds: aws.Int64(3600),
		Name:            aws.String(cuser.Username),
		Policy:          aws.String(policy),
	}

	result, err := svc.GetFederationToken(input)
	if err == nil {

		accessKeyId = *result.Credentials.AccessKeyId
		secretAccessKey = *result.Credentials.SecretAccessKey
		sessionToken = *result.Credentials.SessionToken

	} else {

What if there was an error? This can be caused when an AssumeRole style of profile is used. We check for this on line 61 and under these circumstances, the following sequence of operations occurs. First, we fetch the caller identity. Due to the use of AssumeRole, the ARN representing the caller’s identity is “assumed-role” and contains a random session Id. The code at line 77 checks for “assumed-role” and if it finds it, it replaces it with just “role” (line 78) and then removes the trailing backslash (“/”) and random session Id.

Next, we do an assumeRole on that modified role ARN (line 88). What is actually happening here is that we’re assuming the same role. The account Id of the role remains constant and then name of the role remains constant. When we assumeRole on effectively the same role though, we can retrieve the access key, secret access key and session token required for the next phase.

These are then stored in the variables declared earlier (lines 93 – 95).

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
		input0 := &sts.GetCallerIdentityInput{}
		result0, err := svc.GetCallerIdentity(input0)
		if err != nil {
			panic(fmt.Sprintf("Failed to determine caller identity: %s", err))
		}

		var arn string
		arn = *result0.Arn
		if strings.Contains(arn, "assumed-role") {
			arn = strings.Replace(arn, "assumed-role", "role", 1)
			idx := strings.LastIndex(arn, "/")
			runes := []rune(arn)
			arn = string(runes[0:idx])
		}

		input1 := &sts.AssumeRoleInput{
			RoleArn:         &arn,
			RoleSessionName: &cuser.Username,
		}
		result1, err := svc.AssumeRole(input1)
		if err != nil {
			panic(fmt.Sprintf("Failed to assume role to %s : %s", arn, err))
		}

		accessKeyId = *result1.Credentials.AccessKeyId
		secretAccessKey = *result1.Credentials.SecretAccessKey
		sessionToken = *result1.Credentials.SessionToken
		
	}

Building the URLs

So now (on line 99) we have the necessary credentials. GetFederationToken worked when using a traditional profile with access key and secret access keys. Re-assumeRole worked when an assumeRole profile with a source profile and a role to assume. The code is common from this point onwards.

We next build a URL to generate (get) a sign-in token, and issue an HTTP GET request:

 99
100
101
102
103
104
105
106
107
108
109
110
111
112
        requestParams := "{"
        requestParams += "\"sessionId\":\"" + accessKeyId + "\","
        requestParams += "\"sessionKey\":\"" + secretAccessKey + "\","
        requestParams += "\"sessionToken\":\"" + sessionToken + "\""
        requestParams += "}"

        request_parameters := "?Action=getSigninToken"
        request_parameters += "&DurationSeconds=3600"
        request_parameters += "&SessionType=json"
        request_parameters += "&Session=" + url.QueryEscape(requestParams)

        request_url := signInURL + request_parameters

        response, err := http.Get(request_url)

We check for an error (line 113) and panic if one occurred. In Go that usually means the program exits, although there are ways to recover.

Assuming the request for a sign-in token succeeded, we then read the response into a byte array (called contents). Then we attempt to parse it with a JSON parser into the “s” variable (line 123). Another check for an error. Finally (line 128) a URL is printed that allows for quick AWS Console login which bypasses MFA requirements for the console.

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
	if err != nil {
		panic(fmt.Sprintf("Error generating sign-in token: %s", err))
	} else {
		defer response.Body.Close()
		contents, err := ioutil.ReadAll(response.Body)
		if err != nil {
			panic(fmt.Sprintf("error: %s", err))
		}

		var s = new(SigninTokenResponse)
		err = json.Unmarshal(contents, &s)
		if err != nil {
			panic(fmt.Sprint("Error parsing response: %s\nError: %s", string(contents), err))
		}

		fmt.Println(signInURL + "?Action=login&SigninToken=" +
			url.QueryEscape(s.SigninToken) + "&Destination=" +
			url.QueryEscape(consoleURL))
	}

On the Mac, you can run the Go program from Terminal. Hold Command while double-clicking on the URL to open it in the default browser.

Downloads Downloads for Mac, Windows, Linux.


Tags:  AWS  AWS IAM  Go  AWS Console
Categories:  AWS  IAM

See Also

 Top Ten Tags

AWS (43)   Kinesis (9)   Streams (8)   AWS Console (5)   Go (5)   Analytics (4)   Data (4)   database (4)   Amazon DynamoDB (3)   Amazon Elastic Compute Cloud (EC2) (3)  


All Tags (173)

Disclaimer

All data and information provided on this site is for informational purposes only. cloudninja.cloud makes no representations as to accuracy, completeness, currentness, suitability, or validity of any information on this site and will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use. All information is provided on an as-is basis.

This is a personal weblog. The opinions expressed here represent my own and not those of my employer. My opinions may change over time.