Skip to main content

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

Popular posts from this blog

Test connectivity via a specific network interface

Recently while working on a Private cloud implementation, I came across a scenario where I needed to test connectivity of a node to the AD/DNS via multiple network adapters.  Many of us would know that having multiple network routes is usually done to take care of redundancy. So that if a network adapter goes down, one can use the other network interface to reach out to the node. In order to make it easy for everyone to follow along, below is an analogy for the above scenario: My laptop has multiple network adapters (say Wi-Fi and Ethernet) connected to the same network. Now how do I test connectivity to a Server on the network only over say Wi-Fi network adapter?

PowerShell + SCCM : Run CM cmdlets remotely

Today I saw a tweet about using implicit remoting to load the Configuration Manager on my machine by Justin Mathews . It caught my eye as I have never really tried it, but theoretically it can be done. Note - The second tweet says "Cannot find a provider with the name CMSite", resolution to which is in the Troubleshooting section at the end.

PowerShell : Trust network share to load modules & ps1

Problem Do you have a central network share, where you store all the scripts or PowerShell modules ? What happens if you try to run the script from a network share ? or if you have scripts (local) which invoke scripts or import PowerShell modules stored on this network share ? Well you would see a security warning like below (Note - I have set execution policy as 'Unrestricted' not 'bypass' here): Run a .ps1 from the network share Well this is a similar warning, which you get when you download scripts from Internet. As the message says run Unblock-File cmdlet to unblock the script and then run it, let's try it.