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 if (-not (Test-Path $ZertoSessionsPath -PathType Container -ErrorAction SilentlyContinue)) { mkdir $ZertoSessionsPath } 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 Request-ZertoSessionTokenRefresh { param( [Parameter(Mandatory = $true)][pscustomobject]$ExistingSession, [Parameter(Mandatory = $true)][string]$SessionPath ) $refresh = Request-TokenRefresh -ZertoSession $existingSession if (-not $refresh) { throw 'Refresh token expired' } $now = Get-Date # update the token, refresh token, and their expiration values $existingSession.AccessToken = $refresh.access_token $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)" $existingSession | Export-Clixml -Depth 100 -Path $sessionPath -Force return $existingSession } function Get-ZertoSession { [CmdletBinding(DefaultParameterSetName = 'ListAll')] param( [Parameter(ParameterSetName = 'BySessionName')][string]$SessionName = 'Default', [Parameter(ParameterSetName = 'ListAll')][switch]$List, [Parameter(ParameterSetName = 'ByServerName')][string]$Server ) begin { $existingSession = $null switch ($PSCmdlet.ParameterSetName) { 'BySessionName' { $Server = $ZertoSessions.$SessionName.ZertoServer } 'ListAll' { return } Default {} } if ($PSCmdlet.ParameterSetName -eq 'BySessionName' -or $PSCmdlet.ParameterSetName -eq 'ByServerName' ) { # Is there a valid session saved to disk? if so, load it and return it $sessionPath = Join-Path -Path $ZertoSessionsPath -ChildPath $Server if (Test-Path $sessionPath -ErrorAction SilentlyContinue) { $existingSession = Import-Clixml -Path $sessionPath -ErrorAction Ignore } if ($null -eq $existingSession) { $existingSession = $ZertoSessions.$SessionName } if (-not ($testedOk = Test-ZertoSession -ZertoSession $existingSession) ) { try { $existingSession = Request-ZertoSessionTokenRefresh -ExistingSession $existingSession -SessionPath $sessionPath $refreshed = $true } catch { $existingSession = $null } } if ($null -eq $existingSession) { Remove-Item $sessionPath, "$sessionPath.cookie" -Force -ErrorAction SilentlyContinue } } } process { switch ($PSCmdlet.ParameterSetName) { 'BySessionName' { if ($Global:ZertoSessions.Keys -contains $SessionName) { if ($refreshed -or $testedOk) { $existingSession.ZertoWebSession = Build-ZertoWebSession -sessionPath $sessionPath -existingSession $existingSession $ZertoSessions.$SessionName = $existingSession return $existingSession } $ZertoSessionConfig = $Global:ZertoSessions[$SessionName] } else { throw "$($MyInvocation.MyCommand.Name) - No valid ZertoSession found for name: $SessionName" } } 'ByServerName' { if ($existingSession) { $existingSession.ZertoWebSession = Build-ZertoWebSession -sessionPath $sessionPath -existingSession $existingSession return $existingSession } 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) { if ($ZertoSessionConfig.RefreshTokenExpires -gt $Now) { # 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 ($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')][AllowNull()] [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 } } if ($null -eq $ZertoSession) { return $false } return $ZertoSession.TokenExpires -gt (Get-Date).AddSeconds(10) } } 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 } } |