Tuesday, January 20, 2015

PowerShell + REST API : Basic, CMS & CMSURL Authentication

While working with the REST API endpoints exposed by the leaders in MDM space (Hint - VMware acquired them) , I picked up few things on how to authenticate to the REST endpoints and would like to document those here.

The post is generic about how to use the below Authentication schemes:
  • Basic Authentication
  • Certificate Based Authentication


Basic Authentication

In Basic authentication, we base 64 encode the UserName & Password and pass it in the header.

Pretty straight forward on how to do it in PowerShell, Store the Credentials and then just encode them :


$Credential = Get-Credential
$EncodedUsernamePassword = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($('{0}:{1}' -f $Credential.UserName, $Credential.GetNetworkCredential().Password)))


Once done we need to create the authorization header along with the API Key which will be issued to you (for this you will have to refer to the API documentation of the vendor) and lastly I want the content type to be JSON, so we create the headers hashtable.

Finally we pass this information to the Invoke-RestMethod cmdlet along with the REST API URL , HTTP method used (get, post etc) & the Headers.



$Headers = @{'Authorization' = "Basic $($EncodedUsernamePassword)";'APIKey' = "$APIKey";'Content-type' = 'application/json'}
Invoke-RestMethod -Method Get -Uri '<REST API URL>' -Headers $Headers


Certificate Based Authentication

Using the REST API with Cert based authentication is not much of a hassle if the vendor has it clearly documented. This is more applicable in scenarios where you want to Invoke APIs non-interactively (say from a Schedule task) and this is more secure way ,then storing user credentials to disk and using them.

Usually you will be issued a Certificate for Client Authentication purpose and have to use this Certificate to authenticate against the API. I have worked with the below two signing schemes as of the moment:


  • Content Message Signing (CMS)
  • Content Message Signing URL (CMSURL)

In CMS scheme - the "message content"  is signed with the client certificate using PKCS9 and is then base 64 encoded. This method creates problem with the GET Requests as there is no message content.

In CMSURL scheme - the canonical URI is signed with the client certificate using PKCS9 signing and is then base 64 encoded. This works with all the HTTP methods as we sign the URI not the message content.

Note - Both CMS & CMSURL will be able to work if there is a proxy which SSL offloads, as the two Scheme puts the signature in the Authorization header.

Had to research a bit on how to get the CMS & CMSURL authentication schemes to work for me. Got help from Pradeep who works with the API team to give me a walkthrough on how to generate the Signed content. They had an executable developed using C# which I was able to port to PowerShell code ;)
Below is a function which generates the Authorization Header value for the CMSURL scheme: 


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062

function Get-CMSURLAuthorizationHeader
{
    [CmdletBinding()]
    [OutputType([string])]
    Param
    (
        # Input the URL to be
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [uri]$URL,

        # Specify the Certificate to be used
        [Parameter(Mandatory=$true,
                    ValueFromPipeline)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]
        $Certificate
    )

    Begin
    {
        Write-Verbose -Message '[Get-CMSURLAuthorizationHeader] - Starting Function'
  
    }
    Process
    {
       TRY
       {
            #Get the Absolute Path of the URL encoded in UTF8
            $bytes = [System.Text.Encoding]::UTF8.GetBytes(($Url.AbsolutePath))

            #Open Memory Stream passing the encoded bytes
            $MemStream = New-Object -TypeName System.Security.Cryptography.Pkcs.ContentInfo -ArgumentList (,$bytes-ErrorAction Stop

            #Create the Signed CMS Object providing the ContentInfo (from Above) and True specifying that this is for a detached signature
            $SignedCMS = New-Object -TypeName System.Security.Cryptography.Pkcs.SignedCms -ArgumentList $MemStream,$true -ErrorAction Stop

            #Create an instance of the CMSigner class - this class object provide signing functionality
            $CMSigner = New-Object -TypeName System.Security.Cryptography.Pkcs.CmsSigner -ArgumentList $Certificate -Property @{IncludeOption = [System.Security.Cryptography.X509Certificates.X509IncludeOption]::EndCertOnly} -ErrorAction Stop

            #Add the current time as one of the signing attribute
            $null = $CMSigner.SignedAttributes.Add((New-Object -TypeName System.Security.Cryptography.Pkcs.Pkcs9SigningTime))

            #Compute the Signatur
            $SignedCMS.ComputeSignature($CMSigner)

            #As per the documentation the authorization header needs to be in the format 'CMSURL `1 <Signed Content>'
            #One can change this value as per the format the Vendor's REST API documentation wants.
            $CMSHeader = '{0}{1}{2}' -f 'CMSURL','`1 ',$([System.Convert]::ToBase64String(($SignedCMS.Encode())))
            Write-Output -InputObject $CMSHeader
        }
        Catch
        {
            Write-Error -Exception $_.exception -ErrorAction stop
        }
    }
    End
    {
        Write-Verbose -Message '[Get-CMSURLAuthorizationHeader] - Ending Function'
    }
}

How do you use it , you will follow steps which are similar to below :


#Paste the REST API URL below For Ex: https://host/API/v1/system/admins/search?firstname=Deepak
$Url = '<REST API Url>'

#This is the Client Certificate issued to me and has been imported to the Certificate store on my Machine under Current User store
$Certificate = Get-ChildItem -Path Cert:\CurrentUser\my | Where-Object Subject -eq 'CN=Deepak'

#Prepare the headers before hand
$Headers = @{
                'Authorization' = "$(Get-CMSURLAuthorizationHeader -URL $Url -Certificate $Certificate)";
                'APIKey' = "$APIKey";
                'Content-type' = 'application/json'
            }

#Invoke the awesomeness now
Invoke-RestMethod -Method Get -Uri $Url -Headers $Headers -ErrorAction Stop

Now below is other function which generates the CMS header (haven't tested this out) , the process of using CMS Scheme is almost similar except the fact that the request body is signed in this case:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
function Get-CMSAuthorizationHeader
{
    [CmdletBinding()]
    [OutputType([string])]
    Param
    (
        # Input the URL to be
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$body,

        # Specify the Certificate to be used
        [Parameter(Mandatory=$true,
                    ValueFromPipeline)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]
        $Certificate
    )

    Begin
    {
        Write-Verbose -Message '[Get-CMSAuthorizationHeader] - Starting Function'
   
    }
    Process
    {
       TRY
       {
            #Get the String UTF8 encoded at first
            $bytes = [System.Text.Encoding]::UTF8.GetBytes($body)

            #Open Memory Stream passing the encoded bytes
            $MemStream = New-Object -TypeName System.Security.Cryptography.Pkcs.ContentInfo -ArgumentList (,$bytes-ErrorAction Stop

            #Create the Signed CMS Object providing the ContentInfo (from Above) and True specifying that this is for a detached signature
            $SignedCMS = New-Object -TypeName System.Security.Cryptography.Pkcs.SignedCms -ArgumentList $MemStream,$true -ErrorAction Stop

            #Create an instance of the CMSigner class - this class object provide signing functionality
            $CMSigner = New-Object -TypeName System.Security.Cryptography.Pkcs.CmsSigner -ArgumentList $Certificate -Property @{IncludeOption = [System.Security.Cryptography.X509Certificates.X509IncludeOption]::EndCertOnly} -ErrorAction Stop

            #Add the current time as one of the signing attribute
            $null = $CMSigner.SignedAttributes.Add((New-Object -TypeName System.Security.Cryptography.Pkcs.Pkcs9SigningTime))

            #Compute the Signatur
            $SignedCMS.ComputeSignature($CMSigner)

            #As per the documentation the authorization header needs to be in the format 'CMSURL `1 <Signed Content>'
            #One can change this value as per the format the Vendor's REST API documentation wants.
            $CMSHeader = '{0}{1}{2}' -f 'CMS','`1 ',$([System.Convert]::ToBase64String(($SignedCMS.Encode())))
            Write-Output -InputObject $CMSHeader
        }
        Catch
        {
            Write-Error -Exception $_.exception -ErrorAction stop
        }
    }
    End
    {
        Write-Verbose -Message '[Get-CMSAuthorizationHeader] - Ending Function'
    }
}



Resources :


System.Security.Cryptography.Pkcs Namespace
http://msdn.microsoft.com/en-us/library/System.Security.Cryptography.Pkcs(v=vs.110).aspx

10 comments:

  1. Great content as usual Dexter! Thank you

    ReplyDelete
    Replies
    1. Thanks Guido !
      So much to explore with PowerShell 8-)

      Delete
  2. I think there is an error in the $Headers line. It should read: 'aw-tenant-code' = "$APIKey"

    ReplyDelete
    Replies
    1. Have put the below :
      'APIKey' = "$APIKey";
      as a generic example.

      But Yeah ! aw-tenant-code is the header name if you are making REST calls to the AirWatch Products ;)

      Delete
  3. Using Basic Auth, I get 401 Unauthorized errors when calling API from powershell following your example. I know the username/password is correct and my call is fine because I can do it successfully with Browser. Any ideas?

    Thank you.

    ReplyDelete
    Replies
    1. Hey Eric,
      Can you share the code somewhere and are you hitting the AirWatch Rest API endpoints here ?
      This post does not call it out but this was written with AW REST API in mind.

      Dex

      Delete
  4. Hey There - thanks for this post. First time needing to send credentials to a REST API, and I was struggling to find an easy way to do it. Your snippet worked like a charm. Thanks!

    ReplyDelete
    Replies
    1. You're welcome Jon :)
      Thanks for taking time to comment, it is good to be appreciated.

      Delete
  5. Really well explain. Thanks a lot.
    Cheers !

    ReplyDelete
    Replies
    1. Cheers !
      Thanks, I appreciate you saying that.

      Delete