Tackling Basic RESTful Authentication with PowerShell

In the past, I’ve shown how to write RESTful API calls to VMware NSX and provided some sample code. However, after fielding some questions to folks, I’m taking this opportunity to dive a little deeper into how the basic authentication process works. It’s best to have a target platform to demonstrate against, so I’m using Rubrik’s r300 Hybrid Cloud Appliance and its RESTful API endpoint in this post.

Crafting a Basic Authentication Key-Value Pair

I’ve worked with quite a few different API endpoints supplied by infrastructure vendors, and most of them work in a manner that requires submitting credentials over and receiving back a token. The token is then used for subsequent requests in lieu of the original credentials. Rubrik is no exception to this trend.

When working with any system, however, it’s best to get their API documentation and see how they handle authentication. Basic authentication (“Basic Auth”) seems rather popular because it’s simple, whereas others may choose to use more exotic means (OAuth, HMAC, OAuth2, and so forth). Note that the use of SSL to encrypt the connection between the server and client is critical; I would advise never using Basic Auth over HTTP (plain text).

The idea behind Basic Auth is to send a header key-value pair that contains the credentials necessary to use a RESTful method. Here are the three steps:

  1. Take the string “username:password” and encode it using Base64. The colon between username and password is important, even if there is no password.
  2. Prefix this string with the word Basic, resulting in “Basic <Base64 value>”
  3. A header key of “Authorization” is created, with the above results stored in the value.

After this key-value pair is constructed, it will be placed into the header and look like this:

Authorization: Basic U3BvbmdlQm9iOlNxdWFyZVBhbnRz

But hey, where’s the token at?

Receiving a Token

While some API endpoints will let you stuff the Basic Auth details into the header of every API call (such as VMware NSX), others require that you submit the login credentials to a specific method to receive a token. In Rubrik’s case, we first require that you log in by using a POST to the /login resource with a parameter containing the username and password in the body. Here’s the details from Swagger-UI.

rubrik-swagger-ui-login

The response class shows that a properly formatted call should result in a 200 status (OK) and you’ll get back a status, description, userId and token key-value pair.

Easy enough! Here’s the code I’ve written for the Connect-Rubrik cmdlet found in the Community PowerShell Module for Rubrik. Its purpose in life is to gather credentials and acquire a token that will be used in subsequent cmdlets. While I don’t profess to be any sort of expert in this arena, I did want to share what was created in hopes of sparking a fire in others and learning how to better write this sort of code.

Note: the /login resource does not require Basic Auth because that wouldn’t be possible – you have to first authenticate before you can start using the resulting token. 🙂

#Requires -Version 3
function Connect-Rubrik 
{
    <#  
            .SYNOPSIS
            Connects to Rubrik and retrieves a token value for authentication
            .DESCRIPTION
            The Connect-Rubrik function is used to connect to the Rubrik RESTful API and supply credentials to the /login method. Rubrik then returns a unique token to represent the user's credentials for subsequent calls. Acquire a token before running other Rubrik cmdlets.
            .NOTES
            Written by Chris Wahl for community usage
            Twitter: @ChrisWahl
            GitHub: chriswahl
            .LINK
            https://github.com/rubrikinc/PowerShell-Module
            .EXAMPLE
            Connect-Rubrik -Server 192.168.1.1 -Username admin
            This will connect to Rubrik with a username of "admin" to the IP address 192.168.1.1. The prompt will request a secure password.
            .EXAMPLE
            Connect-Rubrik -Server 192.168.1.1 -Username admin -Password (ConvertTo-SecureString "secret" -asplaintext -force)
            If you need to pass the password value in the cmdlet directly, use the ConvertTo-SecureString function.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true,Position = 0,HelpMessage = 'Rubrik FQDN or IP address')]
        [ValidateNotNullorEmpty()]
        [String]$Server,
        [Parameter(Mandatory = $true,Position = 1,HelpMessage = 'Rubrik username')]
        [ValidateNotNullorEmpty()]
        [String]$Username,
        [Parameter(Mandatory = $true,Position = 2,HelpMessage = 'Rubrik password')]
        [ValidateNotNullorEmpty()]
        [SecureString]$Password

    )

    Process {

        # Allow untrusted SSL certs
        Add-Type -TypeDefinition @"
	    using System.Net;
	    using System.Security.Cryptography.X509Certificates;
	    public class TrustAllCertsPolicy : ICertificatePolicy {
	        public bool CheckValidationResult(
	            ServicePoint srvPoint, X509Certificate certificate,
	            WebRequest request, int certificateProblem) {
	            return true;
	        }
	    }
"@
        [System.Net.ServicePointManager]::CertificatePolicy = New-Object -TypeName TrustAllCertsPolicy

        Write-Verbose -Message 'Build the URI'
        $uri = 'https://'+$Server+':443/login'

        Write-Verbose -Message 'Build the JSON body for Basic Auth'
        $credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $Password        
        $body = @{
            userId   = $Username
            password = $credentials.GetNetworkCredential().Password
        }

        Write-Verbose -Message 'Submit the token request'
        try 
        {
            $r = Invoke-WebRequest -Uri $uri -Method: Post -Body (ConvertTo-Json -InputObject $body)
        }
        catch 
        {
            throw 'Error connecting to Rubrik server'
        }
        $global:RubrikServer = $Server
        $global:RubrikToken = (ConvertFrom-Json -InputObject $r.Content).token
        Write-Verbose -Message "Acquired token: $global:RubrikToken"
        
        Write-Host -Object 'You are now connected to the Rubrik API.'

        Write-Verbose -Message 'Validate token and build Base64 Auth string'
        $auth = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($global:RubrikToken+':'))
        $global:RubrikHead = @{
            'Authorization' = "Basic $auth"
        }

    } # End of process
} # End of function

Breaking Down the Code

The real action starts with line 54. This is where the Uniform Resource Identifier (URI) is constructed to talk to the appropriate resource, which is /login for this example. I’m using variables gathered earlier in the script that are defined in the Param section (server, username, and password). Note that password is a SecureString variable to protect the contents.

Next, the credentials are constructed on lines 57-61. The SecureString variable is a bit tricky to work with, so I first create a PSCredential variable to gather up the username and password, and then break it back down into the key-value pairs required by the /login resource.

Lines 64-74 are where the API call occur using Invoke-WebRequest. As I’ve mentioned in earlier posts, I prefer this cmdlet over Invoke-RestMethod because it returns the entire response, not just the content portion. I’m also using an on-the-fly conversion of the $body hashtable (key-value pairs) into JSON with the super handy ConvertTo-Json cmdlet. Finally, I retrieve the resulting token value from the content of the response.

The final portion involves lines 78-82, which constructs a global variable for future Basic Auth requests. This avoids needing to construct the variable for future calls used by other cmdlets. I had to leverage a bit of native .NET code with [System.Convert]::ToBase64String to encode the string for Basic Auth. In our case, the token becomes the username and there is no password anymore. The results are stored into the key named Authorization as per Basic Auth requirements.

Using Basic Auth in Subsequent Calls

Let’s see how the global variable that contains the Basic Auth value is used for other cmdlets. Using the New-RubrikMount cmdlet as an example, which creates a zero-space clone based on a backup point, we can see the code in action.

#Requires -Version 3
function New-RubrikMount
{
    <#  
            .SYNOPSIS
            Create a new Live Mount from a protected VM
            .DESCRIPTION
            The New-RubrikMount cmdlet is used to create a Live Mount (clone) of a protected VM and run it in an existing vSphere environment.
            .NOTES
            Written by Chris Wahl for community usage
            Twitter: @ChrisWahl
            GitHub: chriswahl
            .LINK
            https://github.com/rubrikinc/PowerShell-Module
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true,Position = 0,HelpMessage = 'Virtual Machine to mount',ValueFromPipeline = $true)]
        [Alias('Name')]
        [ValidateNotNullorEmpty()]
        [String]$VM,
        [Parameter(Mandatory = $true,Position = 1,HelpMessage = 'Backup date in MM/DD/YYYY HH:MM format',ValueFromPipeline = $true)]
        [ValidateNotNullorEmpty()]
        [String]$Date,
        [Parameter(Mandatory = $false,Position = 2,HelpMessage = 'Rubrik FQDN or IP address')]
        [ValidateNotNullorEmpty()]
        [String]$Server = $global:RubrikServer
    )

    Process {

        Write-Verbose -Message 'Validating the Rubrik API token exists'
        if (-not $global:RubrikToken) 
        {
            Write-Warning -Message 'You are not connected to a Rubrik server. Using Connect-Rubrik cmdlet.'
            Connect-Rubrik
        }

        Write-Verbose -Message 'Query Rubrik for the list of protected VM details'
        $uri = 'https://'+$global:RubrikServer+':443/vm?showArchived=false'
        try 
        {
            $r = Invoke-WebRequest -Uri $uri -Headers $global:RubrikHead -Method Get
            $result = (ConvertFrom-Json -InputObject $r.Content) | Where-Object -FilterScript {
                $_.name -eq $VM
            }
            if (!$result) 
            {
                throw 'No VM found with that name.'
            }
            $vmid = $result.id
            $hostid = $result.hostId
        }
        catch 
        {
            throw 'Error connecting to Rubrik server'
        }

        Write-Verbose -Message 'Query Rubrik for the protected VM snapshot list'
        $uri = 'https://'+$global:RubrikServer+':443/snapshot?vm='+$vmid
        try 
        {
            $r = Invoke-WebRequest -Uri $uri -Headers $global:RubrikHead -Method Get
            $result = (ConvertFrom-Json -InputObject $r.Content)
            if (!$result) 
            {
                throw 'No snapshots found for VM.'
            }
        }
        catch 
        {
            throw 'Error connecting to Rubrik server'
        }

        Write-Verbose -Message 'Comparing backup dates to user date'
        $Date = $Date -as [datetime]
        if (!$Date) {throw "You did not enter a valid date and time"}
        foreach ($_ in $result)
            {
            if ((Get-Date $_.date) -lt (Get-Date $Date) -eq $true)
                {
                $vmsnapid = $_.id
                break
                }
            }

        Write-Verbose -Message 'Creating a Live Mount'
        $uri = 'https://'+$global:RubrikServer+':443/job/type/mount'

        $body = @{
            snapshotId     = $vmsnapid
            hostId         = $hostid
            disableNetwork = 'true'
        }

        try 
        {
            $r = Invoke-WebRequest -Uri $uri -Headers $global:RubrikHead -Method Post -Body (ConvertTo-Json -InputObject $body)
            if ($r.StatusCode -ne '200') 
            {
                throw 'Did not receive successful status code from Rubrik for Live Mount request'
            }
            Write-Verbose -Message "Success: $($r.Content)"
        }
        catch 
        {
            throw 'Error connecting to Rubrik server'
        }

    } # End of process
} # End of function

Lines 33-38 are validating that the global token variable exists to make sure that you’ve used Connect-Rubrik in the past. A simple true/false test is all it takes.

The other sections (line 44, 64, and 99) are all using Invoke-WebRequest to the Rubrik API and leverage the $global:RubrikHead variable, which contains the Basic Auth key-value pair required for authentication. Notice that within PowerShell, this is as simple as using -Headers $global:RubrikHead. The header could contain more keys than Authentication, but for my purposes that’s the only key required. The global variable can be re-used across all of the Rubrik cmdlets, although I’m sure there are some better ways to securely store the token, despite the fact that it expires after a short while.

So, there you have it. It’s been fun to learn how to make all of these pieces work via PowerShell, and it’s really exercised my noodle a fair bit. If you have suggestions for improvement, please send me a pull request (PR) or file an issue. 🙂