Automate AWS MFA Credential Renewal with OCR and PowerShell

Problem Statement

Multi-Factor Authentication (MFA) is a relatively easy mechanism to improve the security of your Amazon Web Services (AWS) cloud environment. Instead of logging into the AWS Management Console using a username and password, you also have to provide a time-based one-time password (TOTP).

The same concept applies when authenticating from an AWS SDK, using an IAM User access key ID and secret key, which essentially replace the username / password combination that you use to interactively log into the web console. When you use the Get-STSSessionToken command in the AWS PowerShell SDK to obtain temporary credentials, with an MFA device, the maximum credential lifetime you can specify is 36 hours, or 129,600 seconds. That means you’ll have to renew your credentials every 36 hours, or more frequently.

Although it’s not a lot of work, it’s a minor annoyance to have to switch over to your virtual MFA token, such as Twilio’s Authy, copy the six-digit token code, and then switch back to your terminal to paste the token code into the Get-STSSessionToken command.

Wouldn’t it be nice if this could be automated?

Prerequisites

I’ll point out that, before you proceed, you should have the following items in place:

  • PowerShell Core installed on MacOS
  • The AWS Tools PowerShell modules installed
  • An AWS IAM User account, with an access key ID and secret key
  • Your ~/.aws/credentials file should have a [default] profile configured, with just your access key ID and secret key
  • Virtual MFA device (Authy) configured on your IAM User account

If you’ve got all of this set up, you should be good to go!

Automation Is Possible, and It’s Not Too Hard

It was surprisingly easy to develop a solution to automate the refresh of my AWS Security Token Service (STS) credentials, using PowerShell and a few other utilities. I started out by defining the Get-STSSessionToken command that I would need to invoke every 36 hours, which only accepts a handful of parameters.

IMPORTANT: Make sure to replace the IAM username and AWS account ID with your own values.

$AWSAccountID = '111111111111'
$AWSIAMUsername = 'trevor.sullivan'
$MFADevice = 'arn:aws:iam::{0}:mfa/{1}' -f $AWSAccountID, $AWSIAMUsername
$Params = @{
  SerialNumber = $MFADevice   # The Amazon Resource Name (ARN) to your virtual MFA device
  TokenCode = '123456'        # This is the parameter we're going to automate
  ProfileName = 'default'     # The "default" profile in ~/.aws/credentials
  DurationInSeconds = 129600  # 36 hour lifetime for AWS STS session token
}
$SessionToken = Get-STSSessionToken @Params

None of the parameters are going to change between invocations, except for the TokenCode parameter. That is the six-digit MFA token code, which we are automatically going to retrieve from the Authy Desktop application, instead of having to manually copy and paste it.

Capture the Authy Desktop Window

First, we need to capture the Authy Desktop window, so we can detect the token code. One of the caveats of this solution is that you’ll have to leave Authy Desktop open, and pre-navigated to your AWS MFA token, because this solution doesn’t understand how to navigate the various screens within the Authy program.

In order to capture the Authy Desktop window, we’ll use a few different utilities: an open source getwindowid utility, the built-in open command, and the open source Tesseract optical character recognition (OCR) tool. First, we need to install the two external dependencies with this snippet.

if (-not (brew list) -match '(?=.*getwindowid)(?=.*tesseract)') {
  brew install smokris/getwindowid/getwindowid tesseract
}

Once we’ve installed the dependencies, the following snippet will take care of switching from our terminal over to the Authy Desktop program, and capturing a screenshot of it. The screenshot of the Authy Desktop program is stored in a JPEG file in our home directory. Notice how the open source getwindowid utility is used to find the window ID of Authy, and passes that ID into the built-in MacOS screencapture utility.

Additionally, we insert a 1000 millisecond delay, as the graphics layer seems to do some caching that causes old token codes to be captured, should this extra delay not be present.

$ScreenshotPath = '{0}/authy.jpg' -f $HOME
open "/Applications/Authy Desktop.app"
Start-Sleep -Milliseconds 1000
Start-Process -Wait -FilePath screencapture -ArgumentList ('-x -Jwindow -l{0} {1}' -f (GetWindowID 'Authy Desktop' 'Authy'), $ScreenshotPath)

Retrieving a TOTP with OCR

Now that we’ve captured the Authy Desktop screenshot, we need to perform OCR against the screenshot file, to detect the six-digit token code. To do this, we’ll take advantage of the free and open source Tesseract utility. The command line for invoking Tesseract is very straightforward, with one minor caveat: the resulting file automatically appends the .txt file extension, so you’ll want to leave that off.

$null = tesseract ~/authy.jpg ~/totp

After you’ve invoked Tesseract against the Authy Desktop screenshot, you’ll need to search all of the resulting text for the six-digit token code. Regular expressions are a sensible tool to perform this pattern-based search. We use the Get-Content command to load the text file, and then use PowerShell’s -match operator to search for three, consecutive digit characters, immediately followed by a space, and then another three, consecutive digit characters. Finally, we remove the space in between the two blocks of digits.

The results of this regular expression match will be our six-digit TOTP, which we can now pass back into the Get-STSSessionToken command in the AWS PowerShell SDK.

if ((Get-Content -Raw -Path $TesseractOutput) -match '\d{3} \d{3}') {
  $matches[0] -replace ' ', ''
}
else {
  throw 'Couldn''t find TOTP from Authy Desktop'
}

To simplify this whole process, I’ve wrapped up this TOTP retrieval into a handy, reusable function, that I keep in my PowerShell profile script. Feel free to reuse this in your own scripts. Yes, it’s very crude, but it works well enough for my needs.

function Get-AuthyTOTP {
    $TesseractOutput = '{0}/totp.txt' -f $HOME
    $ScreenshotPath = '{0}/authy.jpg' -f $HOME
    
    # Clean up leftover files, if they exist, to avoid corrupting input
    Remove-Item -Path $TesseractOutput, $ScreenshotPath -ErrorAction Ignore

    if (-not (brew list) -match '(?=.*getwindowid)(?=.*tesseract)') {
      brew install smokris/getwindowid/getwindowid tesseract
    }
      
    open "/Applications/Authy Desktop.app"
    Start-Sleep -Milliseconds 1000
    Start-Process -Wait -FilePath screencapture -ArgumentList ('-x -Jwindow -l{0} {1}' -f (GetWindowID 'Authy Desktop' 'Authy'), $ScreenshotPath)
    $null = tesseract ~/authy.jpg ~/totp
    if ((Get-Content -Raw -Path $TesseractOutput) -match '\d{3} \d{3}') {
      $matches[0] -replace ' ', ''
    }
    else {
      throw 'Couldn''t find TOTP from Authy Desktop'
    }
}

Writing the Session Token to Disk

When you call the Get-STSSessionToken command, you get a response with an object containing your credentials, made up of three parts: 1) access key ID, 2) secret access key, and 3) session token. Instead of keeping our fresh STS session token in memory, we should persist it to disk as soon as it’s renewed. That way, if we spin up other PowerShell processes, the Python boto3 SDK, the AWS CLI tool, or use a third-party program that utilizes AWS credentials, they can reuse these temporary credentials.

Let’s take the session token and persist it into the ~/.aws/credentials file, under a new profile name that we can easily reference in other programs. To do this, we’ll need to format the session token using the TOML (INI) file format. Instead of writing our own code to format, we’ll grab the PsIni module, which was designed to work with INI files.

if (Get-Module -ListAvailable -Name PsIni) {
  Install-Module -Name PsIni -Scope CurrentUser -Force
}

Now that we’ve installed the PsIni module, we need to format the session token object into the expected INI format.

# Create a Hashtable of key-value pairs for the INI section
$AWSCredentials = @{
  aws_access_key_id = $AWSSessionToken.AccessKeyId
  aws_secret_access_key = $AWSSessionToken.SecretAccessKey
  aws_session_token = $AWSSessionToken.SessionToken
}

# Generate the INI section and write it to the AWS credentials file
Set-IniContent -FilePath ~/.aws/credentials -NameValuePairs $AWSCredentials -Sections stelligentmfa | Out-IniFile -FilePath ~/.aws/credentials -Force

I’ve used the name stelligentmfa for my AWS profile name, but you are free to change this to whichever profile name you prefer.

Wrapping It Up

Earlier I posted the Get-AuthyTOTP function, which simply retrieves a six-digit TOTP from Authy Desktop. I also wrote another wrapper function, which can be called without specifying any input parameters, which handles the process of retrieving a fresh AWS session token, and persisting it to disk, as we just discussed in the previous section. Here is that code.

You’ll notice that the last command in this function calls out to Set-AWSCredential, which simply sets our new session token as the globally active credential for the current PowerShell session. If you fire up other PowerShell sessions, or use other SDKs, you will need to make sure that you correctly reference the AWS credentials profile name that you selected (ie. stelligentmfa in my example).

function Update-AWSSessionToken {
  [CmdletBinding()]
  param (
    [string] $TokenCode = (Get-AuthyTOTP)
  )
  if (!$TokenCode -or $TokenCode -notmatch '\d{6}') {
    $TokenCode = Read-Host -Prompt 'Please enter your MFA token code'
  }
  $AWSSessionToken = Get-STSSessionToken -SerialNumber $MFADevice -TokenCode $TokenCode -ProfileName default -DurationInSeconds 129600
 
  $AWSCredentials = @{
    aws_access_key_id = $AWSSessionToken.AccessKeyId
    aws_secret_access_key = $AWSSessionToken.SecretAccessKey
    aws_session_token = $AWSSessionToken.SessionToken
  }
  Set-IniContent -FilePath ~/.aws/credentials -NameValuePairs $AWSCredentials -Sections stelligentmfa | Out-IniFile -FilePath ~/.aws/credentials -Force
  Set-AWSCredential -ProfileName stelligentmfa -Scope Global
}

Isn’t This a Security Risk?

I didn’t write this article for about six months after developing this technique, because I figured some security-minded folks would send backlash my way for “bypassing” MFA security.

However, keep in mind that you’re still much more secure than not having an MFA device at all. Even if your AWS access key ID and secret key were compromised, your virtual MFA device is still private. With Authy, you have to manually authorize each device that you want to synchronize with your account. Better yet, if you’ve applied a “backups password,” your MFA tokens are still locked even after the device is manually approved, until you type in the backups password.

Objection: But what if somebody steals your laptop? They’ll have your Authy MFA device!!!11

Yes, exactly. They’ll have access to your virtual MFA device, regardless of whether or not you’ve automated the retrieval of MFA tokens. In fact, the attacker probably doesn’t even care about your automation, because they can simply open the app themselves. Once again, this isn’t taking away any barriers to your virtual MFA device.

So the answer is no, there is nothing less secure about automating the retrieval of your MFA token codes. We’re simply applying the use of optical character recognition (OCR), and a couple of other utilities, to simplify our lives in a very small, but meaningful way.

If you’re really that worried, use a hardware MFA device and call it a day.