cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
3430
Views
6
Helpful
9
Replies

Powershell API Authorization Encoding

awheeler3
Level 1
Level 1

I am working in an environment where I am unable to load custom modules, so the github solutions really won’t work for me.

The authentication encoding method is a horribly twisted process.

The latest attempt is (Keys and data are from the Authentication example):

$intKey = "■■■■■■■■■■■■■■■■■■■■"
$secretkey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$hostname = "■■■■■■■■■■■■■■■■■■■■■■■■■■■■"
$path = "/accounts/v1/account/list"
$params = 'realname=First%20Last&username=root'
$method = "POST"
$date="Tue, 21 Aug 2012 17:29:18 -0000"

$lines = @($date,$method,$hostname,$path,$params)
$jlines  = [string]::Join("`n", $lines)

$hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1
$hmacsha1.Key = [Text.Encoding]::ASCII.GetBytes($secretkey)
$signature = $hmacsha1.ComputeHash([Text.Encoding]::ASCII.GetBytes($jlines))
$hash_hex = [System.BitConverter]::ToString($signature) -replace '-', ''
$auth = $integration + ":" + $hash_hex
[byte[]]$plainText_bytes = [System.Text.Encoding]::ASCII.GetBytes($auth)
$return = [System.Convert]::ToBase64String($plainText_bytes)
$authorize = "Authorization : Basic " + $return

The Result in the example is:
Authorization: Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5NzgxYjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==

However the result I am getting is:
Authorization: Basic OjBGNzBCRTUzQTE1QkYxMzY3MkIwMkNCQ0EyOTFGODFCREUzREU5RDQ=

Which is definitely not the expected result. An idea why it appears to be encoding differently?

1 Accepted Solution

Accepted Solutions

Michael_Maher
Level 1
Level 1

Its not nice to work with I agree.

Here is a reusable PowerShell function you can use

function New-DuoRequest(){
    param(
        [Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            $apiHost,
        
        [Parameter(Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $apiEndpoint,
        
        [Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            $apiKey,
        
        [Parameter(Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $apiSecret,
        
        [Parameter(Mandatory=$false,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $requestMethod = 'GET',
        
        [Parameter(Mandatory=$false,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            [System.Collections.Hashtable]$requestParams
    )
    $date = (Get-Date).ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss -0000")
    $formattedParams = ($requestParams.Keys | Sort-Object | ForEach-Object {$_ + "=" + [uri]::EscapeDataString($requestParams.$_)}) -join "&"
    
    #DUO Params formatted and stored as bytes with StringAPIParams
    $requestToSign = (@(
        $Date.Trim(),
        $requestMethod.ToUpper().Trim(),
        $apiHost.ToLower().Trim(),
        $apiEndpoint.Trim(),
        $formattedParams
    ).trim() -join "`n").ToCharArray().ToByte([System.IFormatProvider]$UTF8)
 
    $hmacsha1 = [System.Security.Cryptography.HMACSHA1]::new($apiSecret.ToCharArray().ToByte([System.IFormatProvider]$UTF8))
    $hmacsha1.ComputeHash($requestToSign) | Out-Null
    $authSignature = [System.BitConverter]::ToString($hmacsha1.Hash).Replace("-", "").ToLower()

    $authHeader = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(('{0}:{1}' -f $apiKey, $authSignature)))

    $httpRequest = @{
        URI         = ('https://{0}{1}' -f $apiHost, $apiEndpoint)
        Headers     = @{
            "X-Duo-Date"    = $Date
            "Authorization" = "Basic $authHeader"
        }
        Body = $requestParams
        Method      = $requestMethod
        ContentType = 'application/x-www-form-urlencoded'
    }
    
    $httpRequest
}

# Calling the function

$values = @{
    
    apiHost = 'myapi.duo$ecurity.com'
    apiEndpoint     = '/admin/v1/users'
    requestMethod   = 'GET'
    requestParams   = @{username="$env:Username"}
    apiSecret       = '*************'
    apiKey          = '*************'
}
$contructWebRequest = New-DuoRequest @values

# Send the request
$wr = Invoke-WebRequest @contructWebRequest
Write-host "Your User ID is $((($wr.Content | ConvertFrom-Json).response).user_id)"

View solution in original post

9 Replies 9

Amy2
Level 5
Level 5

Phew I just noticed you said the secrets in your post are from the authentication examples provided in the Duo docs! FYI I edited your post to remove the secret key before I saw that, because you should never store or transmit your secrets in an insecure system that can be accessed by the public. This is to protect the integrity and security of your Duo integration (for the folks reading this at home )

Unfortunately I cannot help much with your API question as this is outside the realm of my personal expertise. I’ll take a look at our docs and support cases though and follow up here if I find anything that is helpful for you!

awheeler3
Level 1
Level 1

I have tried a variation of the encryption documented in the duo-psmodule documented on github.
Duo-PSModule/Duo.psm1 at master · mbegan/Duo-PSModule · GitHub
Again all the keys and information are from the Duo Documentation.

$intKey = "■■■■■■■■■■■■■■■■■■■■"
$secretkey = "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep"
$hostname = "■■■■■■■■■■■■■■■■■■■■■■■■■■■■"
$path = "/accounts/v1/account/list"
$params = 'realname=First%20Last&username=root'
$method = "POST"
$date='Tue, 21 Aug 2012 17:29:18 -0000'
$lines = @($date,$method,$hostname,$path,$params)
$jlines  = [string]::Join("`n", $lines)

Output of $jlines at this point is:

Tue, 21 Aug 2012 17:29:18 -0000
POST
■■■■■■■■■■■■■■■■■■■■■■■■■■■■
/accounts/v1/account/list
realname=First%20Last&username=root

[byte[]]$key_bytes = [System.Text.Encoding]::UTF8.GetBytes($secretkey)
[byte[]]$data_bytes = [System.Text.Encoding]::UTF8.GetBytes($jlines)
$hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1
$hmacsha1.Key = $key_bytes
$hash_bytes = $hmacsha1.ComputeHash($data_bytes)
$hash_hex = [System.BitConverter]::ToString($hmacsha1.Hash)
$return = $hash_hex.Replace("-","").ToLower()

The output of $return at this point is:
0f70be53a15bf13672b02cbca291f81bde3de9d4

Which of course does not meet the expected output of RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5NzgxYjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==

Michael_Maher
Level 1
Level 1

Its not nice to work with I agree.

Here is a reusable PowerShell function you can use

function New-DuoRequest(){
    param(
        [Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            $apiHost,
        
        [Parameter(Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $apiEndpoint,
        
        [Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            $apiKey,
        
        [Parameter(Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $apiSecret,
        
        [Parameter(Mandatory=$false,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            $requestMethod = 'GET',
        
        [Parameter(Mandatory=$false,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
            [ValidateNotNull()]
            [System.Collections.Hashtable]$requestParams
    )
    $date = (Get-Date).ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss -0000")
    $formattedParams = ($requestParams.Keys | Sort-Object | ForEach-Object {$_ + "=" + [uri]::EscapeDataString($requestParams.$_)}) -join "&"
    
    #DUO Params formatted and stored as bytes with StringAPIParams
    $requestToSign = (@(
        $Date.Trim(),
        $requestMethod.ToUpper().Trim(),
        $apiHost.ToLower().Trim(),
        $apiEndpoint.Trim(),
        $formattedParams
    ).trim() -join "`n").ToCharArray().ToByte([System.IFormatProvider]$UTF8)
 
    $hmacsha1 = [System.Security.Cryptography.HMACSHA1]::new($apiSecret.ToCharArray().ToByte([System.IFormatProvider]$UTF8))
    $hmacsha1.ComputeHash($requestToSign) | Out-Null
    $authSignature = [System.BitConverter]::ToString($hmacsha1.Hash).Replace("-", "").ToLower()

    $authHeader = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(('{0}:{1}' -f $apiKey, $authSignature)))

    $httpRequest = @{
        URI         = ('https://{0}{1}' -f $apiHost, $apiEndpoint)
        Headers     = @{
            "X-Duo-Date"    = $Date
            "Authorization" = "Basic $authHeader"
        }
        Body = $requestParams
        Method      = $requestMethod
        ContentType = 'application/x-www-form-urlencoded'
    }
    
    $httpRequest
}

# Calling the function

$values = @{
    
    apiHost = 'myapi.duo$ecurity.com'
    apiEndpoint     = '/admin/v1/users'
    requestMethod   = 'GET'
    requestParams   = @{username="$env:Username"}
    apiSecret       = '*************'
    apiKey          = '*************'
}
$contructWebRequest = New-DuoRequest @values

# Send the request
$wr = Invoke-WebRequest @contructWebRequest
Write-host "Your User ID is $((($wr.Content | ConvertFrom-Json).response).user_id)"

Hi Michael, I just wanted to thank you for your post. The script you provided was incredibly helpful in resolving an issue with my company’s environment.

In case it can prove useful to others, here was our problem. When first setting up a directory sync for Duo, I imported the mail attribute from our Active Directory to the alias1 attribute and I imported UserPrincipalName for our username attribute. At the time, these were different values. However we recently began a project to update our UPNs to use a new domain suffix. In the end this caused the UPN to match the values in our mail attributes. In our testing phase we flipped the domain suffix in our UPNs from the old value to the new value, which led to my username (UPN) and alias1 (mail) value being identical. Duo doesn’t permit this, so two things happened when I synced my account from AD to Duo: 1) Duo failed to update my username to the new UPN value and 2) my Duo account was moved to the trash because my previous username was no longer present in AD.

By removing alias1 from our directory sync and customizing the script Michael provided to remove the now read-writable alias1 field, I was able to automate the removal of the no longer needed alias1 attribute from all Duo accounts in our environment.

awheeler3
Level 1
Level 1

When I use the values from the example

$values = @{

■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■'
apiEndpoint     = '/accounts/v1/account/list'
requestMethod   = 'POST'
requestParams   = @{realname="First%20Last";username="root"}
apiSecret       = 'Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep'
apiKey          = '■■■■■■■■■■■■■■■■■■■■'

}

And I force $date to “Tue, 21 Aug 2012 17:29:18 -0000” in the function, I get a value of RElXSjhYNkFFWU9SNU9NQzZUUTE6ZmVmNWZjOTU0MjdhMjhlMDk5ODQyNDYzZWMwOGRmYmE5ZjFlYTJkNA== which does not appear to agree with the Examples value of RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5NzgxYjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==

But it is a lot closer.

I was able to get a successful /auth/v2/check using that function.

lkeyes1
Level 1
Level 1

Confirmed… if I put in my own ikey, skey and Host (■■■■■■■■■■■■■■■■■■■■■■■■■■) the function returns my user ID, or at least I’m assuming it is a user ID; (20 character string uppercase letters and numbers) not sure where to verify it.

awheeler3
Level 1
Level 1

Using that function I was able to generate a working Powershell Lambda for AWS Client VPN Endpoint Client Connection Handler to use Duo for MFA.

Thomas_Powell
Level 1
Level 1

Thanks @Michael_Maher !

If anyone else also is using PowerShell with a different Culture/language:
Delete the $date variable and add these two lines:

    $culture = [CultureInfo]'en-us'
    $date = (Get-Date).ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss -0000", $culture)

and then add -UseBasicParsing to the WebRequest:

$wr = Invoke-WebRequest @contructWebRequest -UseBasicParsing
Getting Started

Find answers to your questions by entering keywords or phrases in the Search bar above. New here? Use these resources to familiarize yourself with the community:

Quick Links