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.
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.
The post is generic about how to use the below Authentication schemes:
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:
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:
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:
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