OktaAWSToken.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
function SetAccount {
  [CmdletBinding()]
  param()
  begin {
    [string]$path = "$PSScriptRoot\OktaAWSToken.config"
    try {
      if (test-path $path -ErrorAction Stop) {
        Write-Verbose "Fetching account data from $path"
        $account = get-content $path -Raw | ConvertFrom-Json
      }
    }
    catch {
      Write-Warning "Config file $PSScriptRoot\OktaAWSToken.config missing."
      throw
    }
  }
  process {
    Write-Verbose "Account data is $account"
    do {
      for ($i = 0; $i -lt ($account.account).count; $i++) {
        write-host "[$($i+1)] $($account.account[$i].name)"
      }
      [int]$selection = Read-Host 'Select an account'

    } while ($selection -lt 1 -or $selection -gt ($account.account).count)
    $prop = @{
      organizationurl = $account.organizationurl
      appurl          = $account.account[($selection - 1)].idp_url
    }
    $oktaaccount = New-Object -TypeName psobject -Property $prop
    Write-Output $oktaaccount
  }
  end {}
} # end SetAccount

function GetSAML {
  [CmdletBinding()]
  param()
  begin {
    $cred = Get-Credential -Message 'OktaAWSToken: Please provide username and password'
    if ($cred -eq $null) {
      Write-Error 'No credential provided'
      throw
    }
    $oktaaccount = SetAccount
    $orgurl = ($oktaaccount.organizationurl -split ('://'))[-1]
    [string]$APIUrl = "https://$orgurl/api/v1/authn"
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.password)
    # Creating the Json object
    $BodyCred = @{"username" = $cred.username; "password" = "$([System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR))"} | ConvertTo-Json
  } # End begin
  process {
    try {
      $OktaSession = Invoke-WebRequest -Uri "$APIUrl" -Method Post -Body $BodyCred -ContentType "application/json" -SessionVariable okta -ErrorAction stop
    }
    catch {
      Write-Error -Message 'Error during authenticating with given username/ password.'
      throw
    }
    $status = (ConvertFrom-Json $OktaSession.Content).status
    write-host "$status"
    if ($status -like 'MFA_REQUIRED') {
      # MFA Auth
      $Content = ConvertFrom-json $OktaSession.Content
      $factor = (ConvertFrom-Json $OktaSession.Content)._embedded.factors
      do {
        for ($i = 0; $i -lt $factor.count; $i++) {
          write-host "[$($i+1)] $($factor[$i].provider) - $($factor[$i].factorType)"
        }
        [int]$selection = Read-Host 'MFA required. Select a MFA method (sms not supported currently)'
      } while ($selection -lt 1 -or $selection -gt $factor.count)
      $MFAUrl = $factor[($selection - 1)]._links.verify.href
      $MFAProvider = $factor[($selection - 1)].provider
      $MFAType = $factor[($selection - 1)].factorType
      switch -Wildcard ($("$MFAProvider$MFAType")) {
        "OKTA*push*" {
          $BodyStateToken = @{stateToken = $Content.stateToken} | ConvertTo-Json
        }
        "Google*token*" {
          $BodyStateToken = @{stateToken = "$($Content.stateToken)"; "passCode" = (Read-Host 'Please enter Google Authenticator Code')} | ConvertTo-Json
        }
        "Okta*token*" {$BodyStateToken = @{stateToken = "$($Content.stateToken)"; "passCode" = (Read-Host 'Please enter Okta Verifier Code')} | ConvertTo-Json
        }
        #"*sms" {$BodyStateToken = @{stateToken = "$($Content.stateToken)"; "passCode" = (Read-Host 'Please enter SMS Code')} | ConvertTo-Json
        #}
      } # end switch
      [string]$MFAStatus = ''
      while ($MFAStatus -notlike 'SUCCESS') {
        $MFAAuth = Invoke-WebRequest -Uri "$MFAUrl" -Method Post -Body $BodyStateToken -ContentType "application/json" -SessionVariable okta
        $MFAStatus = (ConvertFrom-Json  $MFAAuth.Content).status
        Start-Sleep 3
      } # end while

      $OneTimeToken = (ConvertFrom-Json  $MFAAuth.Content).sessiontoken

      $AuthURI = "$($oktaaccount.appurl)?onetimetoken=$OneTimeToken"
      Write-Verbose "$AuthURI is: $AuthURI"
      <#
            The -UseBasicParsing here prevents the script from opening up extra browser session caused by DCOM parsing.
            However, the response isn't fully decoded in that case. Additional steps are taken for decoding.
        #>

      $SamlAuth = Invoke-WebRequest -uri $AuthURI -SessionVariable okta -UseBasicParsing
      $SamlResponse = $SamlAuth.inputfields | Where-Object name -like "saml*" | Select-Object -ExpandProperty value
      Write-Verbose "$SamlResponse is: $SamlResponse"
      $SamlResponse = $SamlResponse.Replace("&#x2b;", "+").Replace("&#x3d;", "=")
      Write-Output $SamlResponse
    } # End if
    elseif ($status -like 'SUCCESS') {
      # Password auth. This part has not be validated.
      $AuthURI = "$($oktaaccount.appurl)"
      Write-Verbose "$AuthURI is: $AuthURI"
      <#
            The -UseBasicParsing here prevents the script from opening up extra browser session caused by DCOM parsing.
            However, the response isn't fully decoded in that case. Additional steps are taken for decoding.
        #>

      $SamlAuth = Invoke-WebRequest -uri $AuthURI -SessionVariable okta -Body $BodyCred -ContentType "application/json" -UseBasicParsing
      $SamlResponse = $SamlAuth.inputfields | Where-Object name -like "saml*" | Select-Object -ExpandProperty value
      Write-Verbose "$SamlResponse is: $SamlResponse"
      $SamlResponse = $SamlResponse.Replace("&#x2b;", "+").Replace("&#x3d;", "=")
      Write-Output $SamlResponse
    } # End elseif
    else {
      Write-Warning 'Error during status checking MFA'
      throw
    } # End else
  }
  end {}
} # end GetSAML

function GetArn {
  [CmdletBinding()]
  param(
    $SamlResponse
  )
  [xml]$SamlResponseDecode = [System.Text.Encoding]::ASCII.GetString([convert]::FromBase64String("$SamlResponse"))
  $arns = $SamlResponseDecode.Response.Assertion.AttributeStatement.Attribute.attributevalue | Select-Object -ExpandProperty "`#text"  | Where-Object {$_ -like 'arn*'}
  Write-Output $arns
} # end GetArn

<#
.SYNOPSIS
The Get-OktaAWSToken retrieves temporary AWS credentials from AWS STS
 
.DESCRIPTION
This is a quick and dirty PowerShell module to provide a programmatic way of retrieving temporary AWS credentials from STS (Security Token Service) when using federated login with Okta Idp with Multi-Factor Authentication (MFA). The following MFA options are supported and tested. The module also includes the password only authentication but never tested. Please give it a try and share your feedback.
 
* Okta Verify
* Okta Verify - Push
* Google Authenticator
The tool will prompt for credentials (MFA supported) to authenticate against Okta. It will then parse the SAML assertion generation (from Okta) and retrieve temporary:
 
* AWS Access Key IDs
* Secret Keys
* AWS Session Token The temporary credential has a default 60 minutes life. You can then use the information to set the AWS Credential for the PowerShell session.
 
.EXAMPLE
In this example, the module is imported manually.
And then we use the Get-OktaAWSToken to pipe the temporary credentail to Set-AWSCredentials cmdlet in the AWSPowerShell module.
 
PS:/> Import-module c:\<path-to-module>\OktaAWSToken
PS:/> Get-OktaAWSToken | Set-AWSCredentials
 
#>

function Get-OktaAWSToken {
  [CmdletBinding()]
  param()
  Write-Verbose -Message 'Getting SAML response'
  $SamlResponse = GetSAML
  Write-Verbose -Message 'Parsing arn'
  [string[]]$arn = GetArn -SamlResponse $SamlResponse
  for ($i = 0; $i -lt $arn.count ; $i++) {
    write-host "[$($i+1)] $($arn[$i].Split('/')[2])"
  }
  Write-Verbose -Message 'Asking user to select a role'
  do {
    [int]$selection = Read-Host "Select the role"
  } while ($selection -lt 1 -or $selection -gt $arn.count)

  $PrincipalArn = $arn[$($selection - 1)].Split(',')[0]
  $RoleArn = $arn[$($selection - 1)].Split(',')[1]

  Write-Verbose -Message 'Hitting the AWS STS'
  $resp = Use-STSRoleWithSAML -PrincipalArn $PrincipalArn -RoleArn $RoleArn  -SAMLAssertion $SamlResponse

  # Ouput the returned credential
  write-output $resp.Credentials
}


Export-ModuleMember -Function Get-OktaAWSToken