lib/Authentication.ps1
## Define a ZertoSessions Variable to store connection details in New-Variable -Scope Global -Name 'ZertoSessions' -Value @{ } -Force New-Variable -Scope Global -Name 'ZertoSessionsPath' -Value (Join-Path -Path $userPaths.credentials -ChildPath '.zertoSessions') -Force function New-ZertoSession { <# .SYNOPSIS Creates a connection to a Zerto VM instance .DESCRIPTION This function creates a connection to the specified Zerto instance using the provided credentials. This session can be used when calling other functions within the Zerto module .PARAMETER SessionName The name that will be used when referring to the created ZertoSession .PARAMETER Server The URI of the Zerto instance to connect to .PARAMETER Credential The credentials to be used twhen connecting to Zerto .PARAMETER AllowInsecureSSL Boolean indicating whether or not an insecure SSL connection is allowed .EXAMPLE $Session = @{ SessionName = 'ZertoDEV' Server = 'zertodev.Zerto.net' Credential = (Get-StoredCredential -Name 'ME') } New-ZertoSession @Session .OUTPUTS None #> [CmdletBinding()] [Alias('Connect-ZertoServer')] param( [Parameter( Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string]$SessionName = 'Default', [Parameter( Mandatory = $true, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName = $true)] [string]$Server, [Parameter( Mandatory = $true, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName = $true)] [int]$Port = 9669, [Parameter( Mandatory = $true, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName = $true)] [pscredential]$Credential, [Parameter( Mandatory = $false, ParameterSetName = 'ByProperty', ValueFromPipelineByPropertyName = $true)] [bool]$AllowInsecureSSL = $false, [Parameter( Mandatory = $false)] [switch]$Passthru ) begin { # If there is no saved session, build the path to the one we will create if (-not ($savedSession = Get-ZertoSession -Server $Server) ) { $sessionPath = Join-Path -Path $ZertoSessionsPath -ChildPath $Server -ErrorAction Stop } } process { if ($savedSession) { $Global:ZertoSessions[$savedSession.Name] = $savedSession return $Passthru.IsPresent ? $savedSession : $null } ## Create a session object for this new connection $NewZertoSession = [ZertoSession]::new($SessionName, $Server, $AllowInsecureSSL) ## Trim the server name $Instance = $Server -replace '^https?://' ## Save the Instance and Port used $NewZertoSession.ZertoServer = $Instance $NewZertoSession.ZertoPort = $Port ## Attempt login to v10 try { $WebRequestSplat = @{ Method = 'POST' Uri = 'https://{0}:{1}/auth/realms/zerto/protocol/openid-connect/token' -f $Instance, $Port SessionVariable = 'ZertoWebSession' PreserveAuthorizationOnRedirect = $true Body = @{ grant_type = 'password' client_id = 'zerto-client' scope = 'openid email profile' username = $Credential.UserName password = $Credential.GetNetworkCredential().Password } SkipCertificateCheck = $AllowInsecureSSL } Write-Verbose "$($MyInvocation.MyCommand.Name) - Web Request Parameters:" Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10 -Compress) Write-Verbose "$($MyInvocation.MyCommand.Name) - Invoking web request" $Response = Invoke-WebRequest @WebRequestSplat -ProgressAction 'SilentlyContinue' Write-Verbose "$($MyInvocation.MyCommand.Name) - Response status code: $($Response.StatusCode)" # Write-Verbose "Response Content: $($Response.Content)" ## Check the Response code for 200 if ($Response.StatusCode -eq 200) { $ResponseContent = $Response.Content | ConvertFrom-Json $ZertoWebSession.Headers['Authorization'] = "Bearer $($ResponseContent.access_token)" $NewZertoSession.ZertoVersion = 10 Write-Verbose "$($MyInvocation.MyCommand.Name) - Now: $(Get-Date)" $NewZertoSession.AccessToken = $ResponseContent.access_token $NewZertoSession.TokenExpires = (Get-Date).AddSeconds($ResponseContent.expires_in) Write-Verbose "$($MyInvocation.MyCommand.Name) - Token $($NewZertoSession.AccessToken.substring($NewZertoSession.AccessToken.Length - 10)) Expires: $($NewZertoSession.TokenExpires)" $NewZertoSession.RefreshToken = $ResponseContent.refresh_token $NewZertoSession.RefreshTokenExpires = (Get-Date).AddSeconds($ResponseContent.refresh_expires_in) Write-Verbose "$($MyInvocation.MyCommand.Name) - Refresh Token Expires: $($NewZertoSession.RefreshTokenExpires)" } elseif (-not $Response) { Write-Verbose "$($MyInvocation.MyCommand.Name) - v10 login failed. Attempting v9 login" # Attempt v9 login Write-Verbose "$($MyInvocation.MyCommand.Name) - Web Request Parameters:" Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10 -Compress) Write-Verbose "$($MyInvocation.MyCommand.Name) - Invoking web request" $WebRequestSplat.Uri = "https://{0}:{1}/v1/session/add" -f $Instance, $Port $Response = Invoke-WebRequest @WebRequestSplat -ProgressAction 'SilentlyContinue' Write-Verbose "$($MyInvocation.MyCommand.Name) - Response status code: $($Response.StatusCode)" Write-Verbose "$($MyInvocation.MyCommand.Name) - Response Content: $($Response.Content)" ## Check the Response code for 200 if ($Response.StatusCode -eq 200) { $ZertoSessionToken = $Response.headers.get_item("x-zerto-session") | Select-Object -First 1 $ZertoWebSession.Headers["x-zerto-session"] = $ZertoSessionToken $ZertoWebSession.Headers.Remove('Authorization') $NewZertoSession.ZertoVersion = 9 } else { throw 'Status Code "{0}" does not indicate success while v9 login' -f $Response.StatusCode } } } catch { Write-Host $_.Exception.Message Write-Host $_.Exception.InnerException.Message throw $_ } ## Add this Session to the ZertoSessions list and save it to disk $NewZertoSession.ZertoWebSession = $ZertoWebSession $cookiePath = "$sessionPath.cookie" $ZertoWebSession.Cookies.GetAllCookies() | Export-Clixml -Path $cookiePath -Depth 10 $NewZertoSession | Export-Clixml -Depth 100 -Path $sessionPath $Global:ZertoSessions[$SessionName] = $NewZertoSession ## Return the session if requested return $Passthru.IsPresent ? $savedSession : $null } } function Build-ZertoWebSession { param ( [Parameter( Mandatory = $true)] [string]$sessionPath, [Parameter( Mandatory = $true)] [PSCustomObject]$existingSession ) $newWebSession = [Microsoft.PowerShell.Commands.WebRequestSession]::new() foreach ($prop in $existingSession.ZertoWebSession.PSObject.Properties | Where-Object Name -NotIn 'Headers', 'Cookies') { $newWebSession.$($prop.Name) = $prop.Value } $newCookie = [System.Net.Cookie]::new() $existingCookies = Import-Clixml -Path "$sessionPath.cookie" foreach ($prop in $existingCookies.PSObject.Properties | Where-Object Name -ne 'TimeStamp') { $newCookie.($prop.Name) = $prop.Value } $newWebSession.Cookies.Add($newCookie) foreach ($header in $existingSession.ZertoWebSession.Headers.GetEnumerator()) { $newWebSession.Headers.Add($header.Key, $header.Value) } return $newWebSession } function Get-ZertoSession { [CmdletBinding(DefaultParameterSetName = 'ListAll')] param( [Parameter(ParameterSetName = 'BySessionName')][string]$SessionName = 'Default', [Parameter(ParameterSetName = 'ListAll')][switch]$List, [Parameter(ParameterSetName = 'ByServerName')][string]$Server ) begin { switch ($PSCmdlet.ParameterSetName) { 'BySessionName' { } 'ByServerName' { # Is there a valid session saved to disk? if so, load it and return it $sessionPath = Join-Path -Path $ZertoSessionsPath -ChildPath $Server try { $existingSession = Import-Clixml -Path $sessionPath -ErrorAction Stop if (Test-ZertoSession -ZertoSession $existingSession) { Write-Verbose "$($MyInvocation.MyCommand.Name) - Returning existing ZertoSession from path: $sessionPath" } else { try { $refresh = Request-TokenRefresh -ZertoSession $existingSession $now = Get-Date $existingSession.AccessToken = $refresh.access_token # update the token, refresh token, and their expiration values $existingSession.RefreshToken = $refresh.refresh_token $existingSession.TokenExpires = $now.AddSeconds($refresh.expires_in) $existingSession.RefreshTokenExpires = $now.AddSeconds($refresh.refresh_expires_in) $existingSession.ZertoWebSession.Headers['Authorization'] = "Bearer $($existingSession.AccessToken)" Write-Verbose ('{3} - Refreshed token. New: {2} Expires at {0}. Refresh exp at {1}' -f $existingSession.TokenExpires, $existingSession.RefreshTokenExpires, $existingSession.AccessToken.substring($existingSession.AccessToken.Length - 10), "$($MyInvocation.MyCommand.Name)") $existingSession | Export-Clixml -Depth 100 -Path $sessionPath -Force } catch { $existingSession = $null } } } catch { $existingSession = $null } if ($null -eq $existingSession) { Write-Verbose "$($MyInvocation.MyCommand.Name) - No valid ZertoSession found at path: $sessionPath" Remove-Item $sessionPath, "$sessionPath.cookie" -Force -ErrorAction SilentlyContinue } } 'ListAll' { return } Default {} } } process { switch ($PSCmdlet.ParameterSetName) { 'BySessionName' { if ($Global:ZertoSessions.Keys -contains $SessionName) { $ZertoSessionConfig = $Global:ZertoSessions[$SessionName] Write-Verbose "$($MyInvocation.MyCommand.Name) - Returning ZertoSession: $($ZertoSessionConfig.Name)@$($ZertoSessionConfig.ZertoServer)" } else { throw "$($MyInvocation.MyCommand.Name) - No valid ZertoSession found for name: $SessionName" } } 'ByServerName' { if ($existingSession) { Write-Verbose "$($MyInvocation.MyCommand.Name) - Returning existing ZertoSession: $($existingSession.Name)@$($existingSession.ZertoServer)" $existingSession.ZertoWebSession = Build-ZertoWebSession -sessionPath $sessionPath -existingSession $existingSession return $existingSession } else { if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Server')) { Write-Verbose "$($MyInvocation.MyCommand.Name) - No valid ZertoSession found at path: $sessionPath for $Server" } } if ($ZertoSessionConfig.ZertoVersion -eq "10") { ## Set time of Now with a bit of an offset to force refresh before expiration $Now = (Get-Date).AddSeconds(-10) if ($ZertoSessionConfig.TokenExpires -lt $Now) { Write-Verbose ("$($MyInvocation.MyCommand.Name) - Token {0} Is Expired" -f $existingSession.AccessToken.substring($existingSession.AccessToken.Length - 10)) if ($ZertoSessionConfig.RefreshTokenExpires -gt $Now) { Write-Verbose "$($MyInvocation.MyCommand.Name) - Token Refresh is still valid. Refreshing Token." # Make the request $uri = "https://{0}:{1}/auth/realms/zerto/protocol/openid-connect/token" $WebRequestSplat = @{ Method = 'POST' Uri = $uri -f $ZertoSessionConfig.ZertoServer, $ZertoSessionConfig.ZertoPort SessionVariable = 'ZertoWebSession' PreserveAuthorizationOnRedirect = $true Body = @{ grant_type = 'refresh_token' client_id = 'zerto-client' refresh_token = $ZertoSessionConfig.RefreshToken } } Write-Verbose "$($MyInvocation.MyCommand.Name) - Web Request Parameters:" Write-Verbose ($WebRequestSplat | ConvertTo-Json -Depth 10) Write-Verbose "$($MyInvocation.MyCommand.Name) - Invoking web request" $CertSettings = @{ SkipCertificateCheck = $ZertoSessionConfig.AllowInsecureSSL } $Response = Invoke-WebRequest @WebRequestSplat @CertSettings -ProgressAction 'SilentlyContinue' Write-Verbose "$($MyInvocation.MyCommand.Name) - Response status code: $($Response.StatusCode)" Write-Verbose "$($MyInvocation.MyCommand.Name) - Response Content: $($Response.Content)" ## Check the Response code for 200 if ($Response.StatusCode -eq 200) { $ResponseContent = $Response.Content | ConvertFrom-Json $ZertoSessionConfig.ZertoWebSession.Headers['Authorization'] = "Bearer $($ResponseContent.access_token)" $ZertoSessionConfig.ZertoVersion = 10 $ZertoSessionConfig.AccessToken = $ResponseContent.access_token $ZertoSessionConfig.TokenExpires = (Get-Date).AddSeconds($ResponseContent.expires_in) $ZertoSessionConfig.RefreshToken = $ResponseContent.refresh_token $ZertoSessionConfig.RefreshTokenExpires = (Get-Date).AddSeconds($ResponseContent.refresh_expires_in) } } } } } 'ListAll' { return $Global:ZertoSessions.Keys } } return $ZertoSessionConfig } } function Remove-ZertoSession { [CmdletBinding()] param( [Parameter( Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string]$SessionName = 'Default' ) ## Check for Existing Session to this server if ($Global:ZertoSessions.Keys -contains $SessionName) { $TheSession = $Global:ZertoSessions[$SessionName] } # $ThisSession.Remove() } function Test-ZertoSession { <# .SYNOPSIS Tests the connection to a ZertoSession .PARAMETER ZertoSession The ZertoSession to test .PARAMETER ZertoSession Test with Server name only .EXAMPLE Test-ZertoSession -ZertoSession (Get-ZertoSession -Name 'Default') .EXAMPLE Test-ZertoSession -Server my.zerto.net .OUTPUTS None #> [CmdletBinding()] param( [Parameter( Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'ZertoSessionObject')] [pscustomobject]$ZertoSession, [Parameter( Mandatory = $true, ParameterSetName = 'ZertoSessionServer')] [string]$Server ) process { if ($PSCmdlet.ParameterSetName -eq 'ZertoSessionServer') { $sessionPath = Join-Path -Path $ZertoSessionsPath -ChildPath $Server if (Test-Path -Path $sessionPath -ErrorAction SilentlyContinue) { $NewZertoSession = Get-Content -Path $sessionPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop return Test-ZertoSession -ZertoSession $NewZertoSession } else { return $false } } return $ZertoSession.TokenExpires -gt (Get-Date).AddSeconds(5) } } function Request-TokenRefresh { param( [Parameter( Mandatory = $true)] [ZertoSession]$ZertoSession ) begin { if ($ZertoSession.RefreshTokenExpires -lt (Get-Date).AddSeconds(5)) { Write-Verbose "$($MyInvocation.MyCommand.Name) - Refresh Token has expired. Will attempt new log in." return } $tokenUrl = "https://{0}:{1}/auth/realms/zerto/protocol/openid-connect/token" -f $ZertoSession.ZertoServer, $ZertoSession.ZertoPort $body = @{ grant_type = 'refresh_token' client_id = 'zerto-client' refresh_token = $ZertoSession.RefreshToken } $refreshSplat = @{ Uri = $tokenUrl Method = 'Post' Body = $body ContentType = 'application/x-www-form-urlencoded' PreserveAuthorizationOnRedirect = $true SkipCertificateCheck = $ZertoSessionConfig.AllowInsecureSSL } } process { if ($ZertoSession.RefreshTokenExpires -lt (Get-Date).AddSeconds(5)) { throw 'Refresh token expired' } $response = Invoke-RestMethod @refreshSplat return $response } } |