Community Spring Cleaning week is here! Join your fellow Maveryx in digging through your old posts and marking comments on them as solved. Learn more here!

Alteryx IO Discussions

Customize and extend the power of Alteryx with SDKs, APIs, custom tools, and more.

API Authentication

LeeMcrae
5 - Atom

Hi there,

 

I’ve been trying to construct an Ansible Playbook (run from our Ansible Automation Platform deployment) which will trigger an Alteryx workflow. Using the information from this page : https://help.alteryx.com/developer-help/server-api-overview I’m still stuck at getting the authentication with the API to work. The guide is a bit unhelpful in that it says just to “Use a 3rd-party signature generation code” – which so far I’ve been unable to find for Ansible.

 

So I’ve been trying to implement my own version (in Powershell/.NET as that’s the language I’m most comfortable in). My idea being that this powershell code to acquire the validated API tokens for the Oauth flow, and then be used later in the playbook to trigger the actual workflow. Unfortunately what I’ve done doesn’t seem to be liked by the Alteryx deployment we have – as all I’m seeing is an HTTP 401 error returned from the server, with the following content:

{"data":null,"exceptionName":"UnauthorizedException","innerExceptionMessage":"","message":"The provided signature(oauth_signature) is invalid."}

I’m not sure what more troubleshooting I can do though. I’ve been following the standard as laid out here : https://oauth.net/core/1.0a/#auth_step1 – but apparently what my code is doing isn’t what the implementation on the Alteryx side is expecting.

 

Does anyone have any alternative solutions I could use from Ansible? Or does anyone have any ideas on better troubleshooting to see where my code is going wrong?

 

Thanks

4 REPLIES 4
patrick_digan
17 - Castor
17 - Castor

Hi @LeeMcrae ! What version of the alteryx server are you on? The oauth1 api end points are deprecated as of 2022.1 which was just released. Oauth2 endpoints are now available as of 21.4 and it's much easier to authenticate.

 

For oauth1, getting the signature just right is tricky. In my experience, it's usually just something small that is preventing auth(like capitalization, order of elements, encoded vs decoded, etc). Check out my post over here https://community.alteryx.com/t5/Engine-Works/Using-the-Alteryx-API-from-Alteryx/ba-p/318565

 

If you're able to post your code, that would be helpful!

LeeMcrae
5 - Atom

Thanks @patrick_digan for your reply.

The version we are on is 2021.3

I will check out your post thank you, and code is as follows:

 

The first section between the two horizontal rules is how I’m calling the code, the bulk of the code is below the second horizontal rule, and is the content of the “oauth1.psm1” file which the first bit references in the “Import-Module” line at the beginning.

___________________________________________________________________________________________________________________________

Import-Module .\oauth1.psm1

$Global:Debugging = $true

Set-Config -ConsumerKey "<REDACTED>" `

           -ConsumerSecret "<REDACTED>" `

           -Url http://bne2-vscsql01/gallery/api/v1/workflows/626772d0969c742ffc3838ab/jobs/

$requestToken = Get-RequestToken

___________________________________________________________________________________________________________________________

#Requires -Version 7

class OauthConfig {
    [String]$ConsumerKey
    [String]$consumerSecret
    [String]$AccessTokenUrl
    [String]$AuthorizeTokenUrl
    [String]$RequestTokenUrl

    # Constructor
    OauthConfig([String] $ConsumerKey, [String] $ConsumerSecret, [String]$url) {
        $this.ConsumerKey = $ConsumerKey
        $this.ConsumerSecret = $ConsumerSecret
        $this.AccessTokenUrl = $Url
        $this.AuthorizeTokenUrl = $Url
        $this.RequestTokenUrl = $Url
    }
}

class RequestTokenInfo {
    [String]$RequestToken
    [String]$RequestTokenSecret
    RequestTokenInfo([String]$RequestToken, [String]$RequestTokenSecret) {
        $this.RequestToken = $RequestToken
        $this.RequestTokenSecret = $RequestTokenSecret
    }
}

class AccessTokenInfo {
    [String]$AccessToken
    [String]$accessTokenSecret
    AccessTokenInfo([String]$AccessToken, [String]$accessTokenSecret) {
        $this.AccessToken = $AccessToken
        $this.AccessTokenSecret = $accessTokenSecret
    }
}

Function Set-Config {
    param(
        [Parameter(Mandatory)]
        [string]$ConsumerKey,
        [Parameter(Mandatory)]
        [string]$ConsumerSecret,
        [Parameter(Mandatory)]
        [string]$Url
    )
    $Global:Config = [OauthConfig]::new($ConsumerKey, $ConsumerSecret, $Url)
}


Function New-Nonce{
    $NewRandom = [System.Random]::New()
    $NewNonce  = $NewRandom.Next(1000000000)
    [string]$NewNonce
}

Function Get-TimeStamp{
    [TimeSpan]$ts = [DateTime]::UtcNow - [DateTime]::new(1970,1,1,0,0,0,0)
    [Convert]::ToInt64($ts.TotalSeconds).ToString()
}

Function Get-QueryParameters{
    param(
        [Parameter(Mandatory)]
        [String]$queryString       
    )
    # Remove any leading '?'
    if($queryString.StartsWith("?")){
        $queryString = $queryString.Remove(0,1)
    }
    # Declare dictionary object to return
    $result = New-Object "System.Collections.Generic.Dictionary[String,String]"

    # If the supplied query string is empty, return the empty dictionary
    if([String]::IsNullOrEmpty($queryString)){
        return $result
    }

    foreach($s in $queryString.Split('&')){
        if(![String]::IsNullOrEmpty($s) -and -not $s.StartsWith("oauth_")){
            if($s.IndexOf('=') -gt 1){
                $temp = $s.Split('=')
                $result.Add($temp[0], $temp[1])
            } else {
                $result.Add($s, [String]::Empty)
            }
        }
    }
    return $result
}

Function Get-NormalizedUrl {
    param(
        [Parameter(Mandatory)]
        [Uri]$uri
    )
    $normUrl = [String]::Format("{0}://{1}", $uri.Scheme, $uri.Host)
    if(!($uri.Scheme -eq "http" -and $uri.Port -eq 80 -or
         $uri.Scheme -eq "https" -and $uri.Port -eq 443)){
            $normUrl += ":" + $uri.Port
    }
    $normUrl += $uri.AbsolutePath
    $normUrl
}

Function ConcatList {
    param(
        [Parameter(Mandatory)]
        [System.Collections.Generic.IEnumerable[String]]$source,
        [Parameter(Mandatory)]
        [String]$separator
    )
    $sb = [Text.StringBuilder]::new()
    foreach($s in $source){
        if($sb.Length -eq 0){
            $sb.Append($s) | Out-Null
        } else {
            $sb.Append($separator) | Out-Null
            $sb.Append($s) | Out-Null
        }
    }
    $sb.ToString()
}

Function Get-RequestUrl{
    param(
        [Parameter(Mandatory)]
        [String]$url
    )
    $uri = [Uri]::new($url, [UriKind]::Absolute)
    $normUrl = [String]::Format("{0}://{1}", $uri.Scheme, $uri.Host)
    if(!($uri.Scheme -eq "http" -and $uri.Port -eq 80 -or
         $uri.Scheme -eq "https" -and $uri.Port -eq 443)){
            $normUrl += ":" + $uri.Port
    }

    $normUrl += $uri.AbsolutePath

    $normUrl
}

Function Get-SignatureBaseString{
    param(
        [Parameter(Mandatory)]
        [String]$method,
        [Parameter(Mandatory)]
        [String]$url,
        [Parameter(Mandatory)]
        [System.Collections.Generic.List[String]]$requestParameters
    )
    $sortedList = [System.Collections.Generic.List[String]]::new($requestParameters)
    $sortedList.Sort()

    $requestParametersSortedString = ConcatList -source $sortedList -separator "&"

    $url = Get-RequestUrl -url $url

    $result = $method.ToUpper() + "&" + [Uri]::EscapeDataString($url) + "&" + `
            [Uri]::EscapeDataString($requestParametersSortedString)

    $result
}

Function Get-Signature{
    param(
        [Parameter(Mandatory)]
        [String]$signatureBaseString,
        [Parameter(Mandatory)]
        [String]$consumerSecret,
        [Parameter(Mandatory=$False)]
        [String]$tokenSecret = [String]::Empty
    )
    $hmacsha1     = [Security.Cryptography.HMACSHA1]::new()
    $key          = [Uri]::EscapeDataString($consumerSecret) + "&" + ([String]::IsNullOrEmpty($tokenSecret) `
                    ? "" `
                    : [Uri]::EscapeDataString($tokenSecret))
    $hmacsha1.Key = [Text.Encoding]::UTF8.GetBytes($key)
    $dataBuffer   = [Text.Encoding]::UTF8.GetBytes($signatureBaseString)
    $hashBytes    = $hmacsha1.ComputeHash($dataBuffer)

    $result       = [Convert]::ToBase64String($hashBytes)
    $result
}

Function Get-AuthorizationHeaderValue{
    param(
        [Parameter(Mandatory)]
        [String]$accessToken,
        [Parameter(Mandatory)]
        [String]$accessTokenSecret,
        [Parameter(Mandatory)]
        [String]$url,
        [Parameter(Mandatory)]
        [Net.Http.HttpMethod]$httpMethod
    )

    $nonce = New-Nonce
    $timeStamp = Get-TimeStamp

    $requestParameters = [System.Collections.Generic.List[String]]::new()
    $requestParameters.Add("oauth_consumer_key=" + $AccessKey)
    $requestParameters.Add("oauth_nonce=" + $nonce)
    $requestParameters.Add("oauth_signature_method=HMAC-SHA1")
    $requestParameters.Add("oauth_timestamp=" + $timeStamp)
    $requestParameters.Add("oauth_token=" + $accessToken)
    $requestParameters.Add("oauth_version=1.0")
    

    $requestUri = [Uri]::new($url, [UriKind]::Absolute)

    if(![String]::IsNullOrWhiteSpace($requestUri.Query)){
        $parameters = Get-QueryParameters -QueryString $requestUri.Query
        foreach($kvp in $parameters){
            $requestParameters.Add($kvp.Key + "=" + $kvp.Value)
        }
    }

    $signatureBaseString = Get-SignatureBaseString($httpMethod.ToString().ToUpper(), $url, $requestParameters)

    $signature = Get-Signature -signatureBaseString $signatureBaseString -secretKey $accessTokenSecret -accesskey $accessToken

    $requestParametersForHeader = [System.Collections.Generic.List[String]]::new()
    $requestParametersForHeader.Add("oauth_consumer_key=`"" + $accessToken) + "`""
    $requestParametersForHeader.Add("oauth_token=`"" + $accessTokenSecret) + "`""
    $requestParametersForHeader.Add("oauth_signature_method=`"HMAC-SHA1`"")
    $requestParametersForHeader.Add("oauth_timestamp=`"" + $timeStamp + "`"")
    $requestParametersForHeader.Add("oauth_nonce=`"" + $nonce + "`"")
    $requestParametersForHeader.Add("oauth_version=`"1.0`"")
    $requestParametersForHeader.Add("oauth_signature=`"" + [Uri]::EscapeDataString($signature) + "`"")

    $result = ConcatList -source $requestParametersForHeader -separator ","
    $result
}

Function Get-AuthorizationHeader{
    param(
        [Parameter(Mandatory)]
        [String]$accessToken,
        [Parameter(Mandatory)]
        [String]$accessTokenSecret,
        [Parameter(Mandatory)]
        [String]$url,
        [Parameter(Mandatory)]
        [Net.Http.HttpMethod]$httpMethod
    )
    $authHeader = [Net.Http.Http.Headers.AuthenticationHeaderValue]::new("OAuth", (Get-AuthorizationHeaderValue -accessToken $accessToken -accessTokenSecret $accessTokenSecret -url $url -httpMethod $httpMethod))
    $authHeader
}

Function Send-PostDataBody {
    param(
        [Parameter(Mandatory)]
        [String]$Url,
        [Parameter(Mandatory)]
        [String]$PostData
    )
    try {
        if($Global:Debugging){
            Invoke-RestMethod -Method Post -Uri $Url -Body $PostData -ContentType "application/x-www-form-urlencoded" -Proxy http://fiddler.local:8888
        } else {
            Invoke-RestMethod -Method Post -Uri $Url -Body $PostData -ContentType "application/x-www-form-urlencoded"
        }
    } catch {
        throw $_
    }
}

Function Send-PostDataHeaders {
    param(
        [Parameter(Mandatory)]
        [String]$Url,
        [Parameter(Mandatory)]
        [String]$PostData
    )
    $Headers = @{
        "Authorization" = [String]::Format("OAuth {0}", $PostData)
    }
    try {
        if($Global:Debugging){
            Invoke-RestMethod -Method Post -Uri $Url -Headers $Headers -ContentType "application/json" -Body '{}' -Proxy http://fiddler.local:8888
        } else {
            Invoke-RestMethod -Method Post -Uri $Url -Headers $Headers -ContentType "application/json" -Body '{}'
        }
    } catch {
        throw $_
    }
}

Function Get-RequestToken {
    [String]$Nonce     = New-Nonce
    [String]$TimeStamp = Get-TimeStamp

    $requestParameters = [System.Collections.Generic.List[String]]::new()
    $requestParameters.Add("oauth_timestamp=" + $TimeStamp)
    $requestParameters.Add("oauth_signature_method=HMAC-SHA1")
    $requestParameters.Add("oauth_consumer_key=" + $Global:Config.ConsumerKey)
    $requestParameters.Add("oauth_version=1.0")
    $requestParameters.Add("oauth_nonce=" + $Nonce)

    $signatureBaseString = Get-SignatureBaseString -method "POST" -url $Global:Config.RequestTokenUrl -requestParameters $requestParameters

    $signature = Get-Signature -signatureBaseString $signatureBaseString -consumerSecret $Global:Config.ConsumerSecret

    $responseText = Send-PostDataHeaders -Url $Global:Config.RequestTokenUrl -PostData ((ConcatList -source $requestParameters -separator ",") + ",oauth_signature=" + [Uri]::EscapeDataString($signature))

    if(![String]::IsNullOrEmpty($responseText)){
        [String]$oauthToken = $null
        [String]$oauthTokenSecret = $null

        [String]$keyValPairs = $responseText.Split('&')

        for($i=0; $i -lt $keyValPairs.Length; $i++){
            [String]$splits = $keyValPairs[$i].Split('=')
            switch ($splits[0]) {
                "oauth_token" { $oauthToken = $splits[1]; Break }
                "oauth_token_secret" { $oauthTokenSecret = $splits[1]; Break }
            }
        }

        $RequestTokenInfo = [RequestTokenInfo]::new($oauthToken, $oauthTokenSecret)
        return $RequestTokenInfo
    }
    throw "Empty response text when getting the request token"
}

 

 

patrick_digan
17 - Castor
17 - Castor

@LeeMcrae The only thing I can think of is removing the port part when you go to create your signature (based on this post). Since you're using the default port, I don't think it's needed anyway. 

LeeMcrae
5 - Atom


Thank you @patrick_digan for taking the time, much appreciated. I'll give it a shot!