MFA_utils.ps1
# Creates an Oath counter # Jun 26th 2020 function Get-OathCounter { [cmdletbinding()] Param() Process { $OathCounter = [int](((Get-Date).ToUniversalTime() - $epoch).TotalSeconds / 30) return $OathCounter } } # Generates a new time-based OTP for MFA # Jun 26th 2020 function Generate-tOTP { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$SecretKey, [Parameter(Mandatory=$False)] [int]$Seconds=0, [Parameter(Mandatory=$False)] [int]$TimeShift=0, [Parameter(Mandatory=$False)] [int]$TimeStep=30, [Parameter(Mandatory=$False)] [int]$Digits=6 ) Process { if ($Digits -le 0) { $Digits = 6 # Can't be zero so default to six } if($Seconds -le 0) { # Can't be zero so default to current time $Seconds = [int]((Get-Date).ToUniversalTime() -$epoch).TotalSeconds } if($TimeStep -le 0) { $TimeStep = 30 # Can't be zero, so default to 30 seconds } $Seconds = ($Seconds + $TimeShift) / $TimeStep [byte[]]$timeBytes = @( 0,0,0,0, # Integer has only 4 bytes so the first four are zeros [byte](([int]$Seconds -shr 24) -band 255), [byte](([int]$Seconds -shr 16) -band 255), [byte](([int]$Seconds -shr 8) -band 255), [byte]( [int]$Seconds -band 255) ) return Generate-hOTP -SecretKey $SecretKey -timeBytes $timeBytes -Digits $Digits } } # Generates a new HMAC based OTP for MFA # Jun 26th 2020 function Generate-hOTP { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$SecretKey, [Parameter(Mandatory=$True)] [byte[]]$TimeBytes, [Parameter(Mandatory=$True)] [int]$Digits, [Parameter(Mandatory=$False)] [int]$Position = 0 ) Begin { $hOtpFullResult = 1073741840 } Process { $divider=0 if ($Digits -ge 1 -and $Digits -le 9) { $divider = [Math]::Pow(10, $Digits) } elseif ($Digits -eq $hOtpFullResult) { $divider = 0 } else { throw "Only 1-9 digits are accepted!" } # Calculate the hash using the secret as a key [byte[]]$decodedSecret = From-Base32String -Secret $SecretKey $HmacSHA1 = [Security.Cryptography.HMACSHA1]::new($decodedSecret) $hmacSize = 20 $hash=$HmacSHA1.ComputeHash($TimeBytes) if ($divider -gt 0) { if ($Position -le 0 -or $Position -ge ($hmacSize - 4)) { $Position = $hash[$hmacSize- 1] -band 15 } # Generate the OTP from the hash $retVal = ($hash[$Position] -band 127) -shl 24 $retVal = $retVal -bor ($hash[$Position + 1] -band 255) -shl 16 $retVal = $retVal -bor ($hash[$Position + 2] -band 255) -shl 8 $retVal = $retVal -bor ($hash[$Position + 3] -band 255) $retVal = $retVal % $divider return $retVal } else { return Convert-ByteArrayToHex -Bytes $hash } } } # Generates a validation code # Jun 27th 2020 function Generate-ValidationCode { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$SecretKey, [Parameter(Mandatory=$False)] [int]$OathCounter=0 ) Process { if ($OathCounter -le 0) { $OathCounter = Get-OathCounter } $validationCode = Generate-tOTP -SecretKey $SecretKey -Digits 1073741840 -Seconds ($OathCounter*30) return $validationCode.toLower() } } # Converts Base32 string to bytes # Jun 26th 2020 # Credits: HumanEquivalentUnit function From-Base32String { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$Secret ) Process { $bigInteger = [Numerics.BigInteger]::Zero foreach ($char in ($secret.ToUpper() -replace '[^A-Z2-7]').GetEnumerator()) { $bigInteger = ($bigInteger -shl 5) -bor ('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.IndexOf($char)) } [byte[]]$secretAsBytes = $bigInteger.ToByteArray() # BigInteger sometimes adds a 0 byte to the end, # if the positive number could be mistaken as a two's complement negative number. # If it happens, we need to remove it. if ($secretAsBytes[-1] -eq 0) { $secretAsBytes = $secretAsBytes[0..($secretAsBytes.Count - 2)] } # BigInteger stores bytes in Little-Endian order, # but we need them in Big-Endian order. [array]::Reverse($secretAsBytes) return [byte[]]$secretAsBytes } } # Converts Base32 string to bytes # Jun 26th 2020 # Credits: HumanEquivalentUnit function To-Base32String { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [byte[]]$Secret ) Process { $byteArrayAsBinaryString = -join $Secret.ForEach{ [Convert]::ToString($_, 2).PadLeft(8, '0') } $Base32Secret = [regex]::Replace($byteArrayAsBinaryString, '.{5}', { param($Match) 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[[Convert]::ToInt32($Match.Value, 2)] }) return $Base32Secret } } # Parses authentication apps data # Jun 27th 2020 function Parse-AuthApps { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] $appDetails ) Process { $apps=@() foreach($app in $appDetails) { $appAtributes = [ordered]@{ "AuthenticationType"=$app.AuthenticationType "DeviceName"= $app.DeviceName "DeviceTag"= $app.DeviceTag "DeviceToken"= $app.DeviceToken "Id"= $app.Id "NotificationType"= $app.NotificationType "OathTokenTimeDrift"=$app.OathTokenTimeDrift "OathSecretKey"= $app.OathTokenSecretKey "PhoneAppVersion"= $app.PhoneAppVersion "TimeInterval"= $app.TimeInterval "LastAuthTime" = $(if($app.lastAuthenticatedTimestamp){[DateTime]$app.lastAuthenticatedTimestamp}else{$null}) } $apps+=New-Object psobject -Property $appAtributes } return $apps } } # Gets MFA App Registration information (i.e. url, activation code, and session context) # Jul 1st 2020 function Get-MFAAppRegistrationInfo { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$False)] [ValidateSet("APP","OTP")] [String]$Type="APP" ) Process { # Create the headers $headers=@{ "Authorization" = "Bearer $AccessToken" "Content-Type" = "application/json" "User-Agent" = Get-Setting -Setting "User-Agent" } # Registration type if($Type -eq "APP") { $securityInfoType = 2 } elseif($Type -eq "OTP") { $securityInfoType = 3 } try { # Get the authorization information $response = Invoke-RestMethod -UseBasicParsing -Uri "https://account.activedirectory.windowsazure.com/securityinfo/Authorize" -Method POST -Headers $headers } catch { throw $_ } # Strip the carbage from the start and convert to psobject $response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json # Verbose Write-Verbose "Authorization information" Write-Verbose " Is authorized: $($response.isAuthorized)" Write-Verbose " Require MFA: $($response.requireMfa)" Write-Verbose " Require NGC MFA: $($response.requireNgcMfa)" Write-Verbose " Prompt for login: $($response.promptForLogin)" Write-Verbose " My star enabled: $($response.isMyStarEnabled)" # Check is user authorized to register MFA with this access token if(!$response.isAuthorized) { throw "User is not authorized to register MFA! Use -Verbose for details." } # Extract the session context and update headers $sessionCtx = $response.sessionCtx $headers["SessionCtx"] = $sessionCtx # Get the needed codes $response = Invoke-RestMethod -UseBasicParsing -Uri "https://account.activedirectory.windowsazure.com/securityinfo/InitializeMobileAppRegistration" -Method POST -Headers $headers -Body "{""securityInfoType"": $securityInfoType}" # Strip the carbage from the start and convert to psobject $response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json # Add the session context to return value $response | Add-Member -NotePropertyName "SessionCtx" -NotePropertyValue $sessionCtx Write-Verbose "Registration info:`n$response" # Return return $response } } # Sends a new MFA App activation # Jul 2nd 2020 function Send-MFAAppNewActivation { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$True)] $RegistrationInfo, [Parameter(Mandatory=$True)] [String]$DeviceToken, [Parameter(Mandatory=$False)] [String]$DeviceName="AADInternals" ) Process { $url = $RegistrationInfo.Url # Append the PfPaWs.asmx if not included if(!$Url.EndsWith($PfPaWs)) { if(!$Url.EndsWith("/")) { $Url+="/"; } $Url+=$PfPaWs; } # Create the headers $headers=@{ "SOAPAction" = "http://www.phonefactor.com/PfPaWs/ActivateNew" "Content-Type" = "text/xml; charset=utf-8" "User-Agent" = "Dalvik/2.1.0 (Linux; U; Android 8.1.0; AADInternals)" } $body=@" <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns4="http://www.phonefactor.com/PfPaWs"> <soap:Header/> <soap:Body> <ns4:ActivateNew> <ns4:activationParams> <ns4:ActivationCode>$($RegInfo.ActivationCode)</ns4:ActivationCode> <ns4:DeviceToken>$DeviceToken</ns4:DeviceToken> <ns4:DeviceName>$DeviceName</ns4:DeviceName> <ns4:OathCounter>$(Get-OathCounter)</ns4:OathCounter> <ns4:Version>$Version</ns4:Version> </ns4:activationParams> </ns4:ActivateNew> </soap:Body> </soap:Envelope> "@ # Send the activation request $response = Invoke-RestMethod -UseBasicParsing -Uri $Url -Method POST -Headers $headers -Body $body # Extract the activation information $activationInformation=$response.Envelope.Body.ActivateNewResponse.activationInfo Write-Verbose "Activation info:`n$activationInformation" # Return return $activationInformation } } # Sends a new MFA App activation confirmation # Jul 2nd 2020 function Send-MFAAppNewActivationConfirmation { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$True)] $RegistrationInfo, [Parameter(Mandatory=$True)] $ActivationInfo ) Process { $url = $RegistrationInfo.Url # Append the PfPaWs.asmx if not included if(!$Url.EndsWith($PfPaWs)) { if(!$Url.EndsWith("/")) { $Url+="/"; } $Url+=$PfPaWs; } # Create the headers $headers=@{ "SOAPAction" = "http://www.phonefactor.com/PfPaWs/ConfirmActivation" "Content-Type" = "text/xml; charset=utf-8" "User-Agent" = "Dalvik/2.1.0 (Linux; U; Android 8.1.0; AADInternals)" } # Create the body $body=@" <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns4="http://www.phonefactor.com/PfPaWs"> <soap:Header/> <soap:Body> <ns4:ConfirmActivation> <ns4:confirmationCode>$($ActivationInfo.ConfirmationCode)</ns4:confirmationCode> </ns4:ConfirmActivation> </soap:Body> </soap:Envelope> "@ # Send the activation confirmation $response = Invoke-RestMethod -UseBasicParsing -Uri $Url -Method POST -Headers $headers -Body $body Write-Verbose "Confirmation Activation: $($response.Envelope.Body.ConfirmActivationResponse.ConfirmActivationResult)" # Return return $response.Envelope.Body.ConfirmActivationResponse.ConfirmActivationResult -eq "true" } } # Adds the new device # Jul 2nd 2020 function Add-MFAAppAddDevice { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$True)] $RegistrationInfo, [ValidateSet("APP","OTP")] [String]$Type="APP" ) Process { # Set the headers $headers=@{ "Authorization" = "Bearer $AccessToken" "SessionCtx" = $RegistrationInfo.SessionCtx #"Access-Control-Request-Method" = "POST" #"Access-Control-Request-Headers" ="ajaxrequest,authorization,content-type,sessionctx" "Origin" = "https://mysignins.microsoft.com" "Sec-Fetch-Site" = "cross-site" "Sec-Fetch-Mode" = "cors" "Sec-Fetch-Dest" = "empty" "Content-Type" = "application/json" } if($Type -eq "APP") { $securityType = 2 $secretKey = $RegistrationInfo.ActivationCode } elseif($Type -eq "OTP") { $securityType = 3 $secretKey = $RegistrationInfo.SecretKey } $body="{""Type"":$securityType,""Data"":""{\""secretKey\"":\""$secretKey\"",\""affinityRegion\"":\""$($RegistrationInfo.AffinityRegion)\""}""}" $state=0 $counter=0 # Loop until we get the context or fail for 10 times while($state -ne 1 -and ($counter++) -lt 11) { # Wait Start-Sleep -Seconds 2 Write-Verbose "Adding MFA application #$counter" # Send the AddSecurityInfo request $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://account.activedirectory.windowsazure.com/securityinfo/AddSecurityInfo" -Headers $headers -Body $body # Strip the carbage from the start and convert to psobject $response=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json # Get the verification state $state = $response.VerificationState } Write-Verbose "Verification context: $($response.VerificationContext)" # Return return $response.VerificationContext } } # Sends MFA App verification request # Jul 2nd 2020 function Verify-MFAAppAddDevice { [cmdletbinding()] Param( [Parameter(Mandatory=$True)] [String]$AccessToken, [Parameter(Mandatory=$True)] $RegistrationInfo, [Parameter(Mandatory=$True)] [String]$VerificationContext, [ValidateSet("APP","OTP")] [String]$Type="APP" ) Process { # Set the headers $headers=@{ "Authorization" = "Bearer $AccessToken" "SessionCtx" = $RegistrationInfo.SessionCtx #"Access-Control-Request-Method" = "POST" #"Access-Control-Request-Headers" ="ajaxrequest,authorization,content-type,sessionctx" "Origin" = "https://mysignins.microsoft.com" "Sec-Fetch-Site" = "cross-site" "Sec-Fetch-Mode" = "cors" "Sec-Fetch-Dest" = "empty" "Content-Type" = "application/json" } if($Type -eq "APP") { $securityType = 2 $verificationData = $null } elseif($Type -eq "OTP") { $securityType = 3 $verificationData = (New-AADIntOTP -SecretKey $RegistrationInfo.SecretKey).OTP.replace(" ","") Start-Sleep -Seconds 3 } # Create the body $body = "{""Type"":$securityType,""VerificationContext"":""$VerificationContext"",""VerificationData"":$verificationData}" $state=0 $counter=0 $dataUpdates="" # Loop until we get the data or fail for 10 times while([string]::IsNullOrEmpty($dataUpdates) -and ($counter++) -lt 11) { # Wait Start-Sleep -Seconds 2 Write-Verbose "Sending VerifySecurityInfo message #$counter" # Send the VerifySecurityInfo message $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://account.activedirectory.windowsazure.com/securityinfo/VerifySecurityInfo" -Headers $headers -Body $body # Strip the carbage from the start and convert to psobject $responseBody=$response.Substring($response.IndexOf("{")-1) | ConvertFrom-Json # Get the verification state $dataUpdates = $responseBody.Dataupdates } Write-Verbose "Data Updates: $dataUpdates" # Return return $dataUpdates } } |