Private/SavedContext.ps1

# The saved sign-in (VM-6423): what signs a new PowerShell window in without a browser.
#
# A session lives in this module's scope only (Private/Session.ps1), and its refresh token cannot be shared:
# it is single-use, so two windows holding one would revoke each other's session family the moment the second
# one refreshed. What is saved instead is a saved sign-in, a credential the server exchanges for a new session
# (POST /api/v1/cli/token, grant stored_grant) without spending it: every window that loads it gets a session
# and a refresh chain of its own, and none signs another out. It acts as the signed-in user in the tenant it
# was saved for, and ends with the sign-in it was saved from (30 days at most). An API token (Connect-VMetric
# -ApiToken) is saved as it is: it is never refreshed, so windows can share it.
#
# The credential is kept in a Microsoft.PowerShell.SecretManagement vault, the default one, as one SecureString
# secret named VirtualMetric.SavedContext that holds JSON. SecretManagement stays optional: the module never
# imports it unless a sign-in is saved or loaded, and without it, or without a registered vault, nothing is
# saved and the module behaves as before.
#
# Which vault holds it is recorded outside the vault, in a small file that holds no secret (the marker,
# Get-VMetricSavedContextPath): a window looks into a vault only when the marker says a sign-in is saved
# there, so a vault that asks for its password is never opened for a user who saved nothing.
#
# The first VirtualMetric cmdlet a new window runs loads it (Get-VMetricSession), once, unless the window has
# signed in already. Saving another one in its place (Connect-VMetric -Persist, Save-VMetricContext) retires
# the one it replaces on the server, leaving the windows that loaded it signed in; Disconnect-VMetric signs it
# out on the server, which ends every session it minted, and removes it.

$script:VMetricSavedSecretName = 'VirtualMetric.SavedContext'
$script:VMetricSavedContextVersion = 1
# Pending until this window has loaded the saved sign-in or decided not to (Done). The tests set Disabled.
$script:VMetricSavedContextState = 'Pending'
# Where the marker is, when set: the tests point it into their own folder.
$script:VMetricSavedContextPath = $null

function Get-VMetricSavedContextPath {
    # The marker's path: $env:VMETRIC_SAVED_CONTEXT_PATH, else VirtualMetric/PowerShell/SavedContext.json in the
    # user's local application data.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    if ($script:VMetricSavedContextPath) {
        return $script:VMetricSavedContextPath
    }
    if ($env:VMETRIC_SAVED_CONTEXT_PATH) {
        return $env:VMETRIC_SAVED_CONTEXT_PATH
    }
    $root = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)
    if (-not $root) {
        $root = Join-Path -Path $HOME -ChildPath '.local/share'
    }
    Join-Path -Path $root -ChildPath 'VirtualMetric/PowerShell/SavedContext.json'
}

function Get-VMetricSavedContextMarker {
    # The marker: the vault that holds the saved sign-in, and the id it was saved under; $null when no
    # sign-in is saved, or the marker is not one this module wrote.
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    $path = Get-VMetricSavedContextPath
    if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
        return $null
    }
    try {
        $marker = ConvertFrom-VMetricJson -Json ([System.IO.File]::ReadAllText($path))
    }
    catch {
        Write-Verbose "Ignoring $path, which is not the module's record of a saved sign-in: $($_.Exception.Message)"
        return $null
    }
    $vault = [string](Get-VMetricMember -InputObject $marker -Name 'vault')
    if ([long](Get-VMetricMember -InputObject $marker -Name 'version') -ne $script:VMetricSavedContextVersion -or -not $vault) {
        Write-Verbose "Ignoring $path, written by another version of the module."
        return $null
    }
    [pscustomobject]@{
        Vault   = $vault
        SavedId = [string](Get-VMetricMember -InputObject $marker -Name 'savedId')
    }
}

function Read-VMetricSavedContextMarker {
    # The vault the marker names; $null when no sign-in is saved.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $marker = Get-VMetricSavedContextMarker
    if ($marker) {
        return $marker.Vault
    }
    return $null
}

function Write-VMetricSavedContextMarker {
    # Records which vault holds the saved sign-in, and the random id it was saved under. Neither is a
    # secret.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that save a sign-in ask ShouldProcess.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Vault,

        [Parameter(Mandatory)]
        [string] $SavedId
    )

    $path = Get-VMetricSavedContextPath
    $folder = Split-Path -Path $path -Parent
    if ($folder -and -not (Test-Path -LiteralPath $folder -PathType Container)) {
        $null = New-Item -ItemType Directory -Path $folder -Force -ErrorAction Stop
    }
    $text = ConvertTo-VMetricJson -InputObject ([ordered]@{ version = $script:VMetricSavedContextVersion; vault = $Vault; savedId = $SavedId })
    [System.IO.File]::WriteAllText($path, $text, [System.Text.UTF8Encoding]::new($false))
}

function Test-VMetricSessionPersisted {
    # Whether new windows sign in with a session's sign-in: the marker names the id the session saved, or
    # loaded, its sign-in under. Another window that signs the saved sign-in out, or saves another in its
    # place, changes the marker, so the answer is never a stale copy of what this window last did.
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Session
    )

    if (-not $Session.SavedId) {
        return $false
    }
    $marker = Get-VMetricSavedContextMarker
    return [bool]($marker -and $marker.SavedId -eq $Session.SavedId)
}

function Remove-VMetricSavedContextMarker {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess.')]
    [CmdletBinding()]
    param()

    $path = Get-VMetricSavedContextPath
    if (Test-Path -LiteralPath $path -PathType Leaf) {
        Remove-Item -LiteralPath $path -Force -ErrorAction Stop
    }
}

function Test-VMetricSecretManagement {
    # Whether Microsoft.PowerShell.SecretManagement can be used here: imported, or installed and importable.
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    if (Get-Module -Name Microsoft.PowerShell.SecretManagement) {
        return $true
    }
    if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.SecretManagement)) {
        return $false
    }
    try {
        Import-Module -Name Microsoft.PowerShell.SecretManagement -ErrorAction Stop -Verbose:$false
        return $true
    }
    catch {
        Write-Verbose "Microsoft.PowerShell.SecretManagement could not be imported: $($_.Exception.Message)"
        return $false
    }
}

# The calls into SecretManagement, one seam each, which the tests replace.

function Get-VMetricSecretVaultList {
    [CmdletBinding()]
    param()

    Microsoft.PowerShell.SecretManagement\Get-SecretVault -ErrorAction Stop
}

function Read-VMetricSavedSecret {
    # The saved sign-in's text in a vault; $null when the vault holds none.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Vault
    )

    $info = @(Microsoft.PowerShell.SecretManagement\Get-SecretInfo -Name $script:VMetricSavedSecretName -Vault $Vault -ErrorAction Stop |
            Where-Object { $_.Name -eq $script:VMetricSavedSecretName })
    if ($info.Count -eq 0) {
        return $null
    }
    [string](Microsoft.PowerShell.SecretManagement\Get-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -AsPlainText -ErrorAction Stop)
}

function Write-VMetricSavedSecret {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that save a sign-in ask ShouldProcess.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Vault,

        [Parameter(Mandatory)]
        [System.Security.SecureString] $Secret
    )

    Microsoft.PowerShell.SecretManagement\Set-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -SecureStringSecret $Secret `
        -ErrorAction Stop -Confirm:$false -WhatIf:$false
}

function Remove-VMetricSavedSecret {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Vault
    )

    Microsoft.PowerShell.SecretManagement\Remove-Secret -Name $script:VMetricSavedSecretName -Vault $Vault -ErrorAction Stop -Confirm:$false -WhatIf:$false
}

function Get-VMetricSaveVault {
    # The vault a sign-in is saved to: SecretManagement's default vault, or the only one registered. An error
    # that says what to install or register when there is none.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $setup = 'Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore; Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault'
    if (-not (Test-VMetricSecretManagement)) {
        throw (New-VMetricException -Message "Keeping the sign-in for new windows needs Microsoft.PowerShell.SecretManagement and a vault, and the module is not installed. To set one up: $setup" `
                -Code 'SecretManagementUnavailable' -Category NotInstalled -RecommendedAction $setup)
    }
    $vaults = @(Get-VMetricSecretVaultList)
    $default = @($vaults | Where-Object { $_.IsDefault })
    if ($default.Count -gt 0) {
        return [string]$default[0].Name
    }
    if ($vaults.Count -eq 1) {
        return [string]$vaults[0].Name
    }
    if ($vaults.Count -eq 0) {
        throw (New-VMetricException -Message "Keeping the sign-in for new windows needs a SecretManagement vault, and none is registered. To register one: $setup" `
                -Code 'SecretVaultUnavailable' -Category NotInstalled -RecommendedAction $setup)
    }
    throw (New-VMetricException -Message 'Several SecretManagement vaults are registered and none is the default: name one with Set-SecretVaultDefault.' `
            -Code 'SecretVaultUnavailable' -Category NotSpecified -RecommendedAction 'Set-SecretVaultDefault -Name <vault>')
}

function ConvertFrom-VMetricSavedContext {
    # The saved sign-in's JSON as an object with its secret as a SecureString, its addresses validated; an error
    # for text this module did not write or cannot use.
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)]
        [string] $Text,

        [Parameter(Mandatory)]
        [string] $Vault
    )

    $data = ConvertFrom-VMetricJson -Json $Text
    $version = [long](Get-VMetricMember -InputObject $data -Name 'version')
    if ($version -ne $script:VMetricSavedContextVersion) {
        throw (New-VMetricException -Message "The saved sign-in in vault '$(ConvertTo-VMetricSafeText -Text $Vault)' was written by another version of the module (format $version)." `
                -Code 'SavedContextUnsupported' -Category InvalidData)
    }
    $method = [string](Get-VMetricMember -InputObject $data -Name 'authMethod')
    $secret = [string](Get-VMetricMember -InputObject $data -Name 'secret')
    if ($method -notin @('SavedSignIn', 'ApiToken') -or -not $secret) {
        throw (New-VMetricException -Message "The saved sign-in in vault '$(ConvertTo-VMetricSafeText -Text $Vault)' is not one this module can use." `
                -Code 'SavedContextInvalid' -Category InvalidData)
    }
    # Validated as an address given to Connect-VMetric is: https, plain http to this machine only.
    $apiUrl = ConvertTo-VMetricApiBase -Url ([string](ConvertTo-VMetricUrl -Uri ([string](Get-VMetricMember -InputObject $data -Name 'apiUrl')) -What 'the saved API address' -Strict))
    $consoleUrl = ([string](ConvertTo-VMetricUrl -Uri ([string](Get-VMetricMember -InputObject $data -Name 'consoleUrl')) -What 'the saved console address' -Strict -Origin)).TrimEnd('/')
    [pscustomobject]@{
        Vault                = $Vault
        AuthMethod           = $method
        ApiUrl               = $apiUrl
        ConsoleUrl           = $consoleUrl
        SkipCertificateCheck = [bool](Get-VMetricMember -InputObject $data -Name 'skipCertificateCheck')
        Secret               = ConvertTo-VMetricSecureString -String $secret
        GrantId              = [string](Get-VMetricMember -InputObject $data -Name 'grantId')
        SavedId              = [string](Get-VMetricMember -InputObject $data -Name 'savedId')
        ExpiresAt            = [long](Get-VMetricMember -InputObject $data -Name 'expiresAt')
        TenantName           = [string](Get-VMetricMember -InputObject $data -Name 'tenantName')
        Email                = [string](Get-VMetricMember -InputObject $data -Name 'email')
    }
}

function Get-VMetricSavedContext {
    # The saved sign-in the marker names: its vault, addresses and secret; $null when none is saved. An error
    # when the vault cannot be read or holds what this module cannot use.
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

    $vault = Read-VMetricSavedContextMarker
    if (-not $vault) {
        return $null
    }
    if (-not (Test-VMetricSecretManagement)) {
        throw (New-VMetricException -Message "A sign-in is saved in vault '$(ConvertTo-VMetricSafeText -Text $vault)', but Microsoft.PowerShell.SecretManagement is not installed to read it." `
                -Code 'SecretManagementUnavailable' -Category NotInstalled)
    }
    try {
        $text = Read-VMetricSavedSecret -Vault $vault
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        throw (New-VMetricException -Message "Could not read the saved sign-in from vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)" `
                -Code 'SavedContextUnreadable' -Category ReadError -InnerException $_.Exception)
    }
    if (-not $text) {
        # Removed from the vault by hand: the marker is all that is left.
        Write-Verbose "Vault '$(ConvertTo-VMetricSafeText -Text $vault)' holds no saved sign-in any more."
        Remove-VMetricSavedContextMarker
        return $null
    }
    ConvertFrom-VMetricSavedContext -Text $text -Vault $vault
}

function Test-VMetricSameSecret {
    # Whether two SecureStrings hold the same text, compared in constant time.
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [AllowNull()]
        [System.Security.SecureString] $Left,

        [AllowNull()]
        [System.Security.SecureString] $Right
    )

    if (-not $Left -or -not $Right) {
        return $false
    }
    $leftBytes = [System.Text.Encoding]::UTF8.GetBytes((ConvertFrom-VMetricSecureString -SecureString $Left))
    $rightBytes = [System.Text.Encoding]::UTF8.GetBytes((ConvertFrom-VMetricSecureString -SecureString $Right))
    return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals($leftBytes, $rightBytes)
}

function Remove-VMetricSavedContextCopy {
    # Removes a saved sign-in from its vault, and the marker, unless the vault holds another one by now (a newer
    # save in another window): what is removed is only ever the one this window read.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that remove a sign-in ask ShouldProcess; the loader removes only a saved sign-in the server refused.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject] $Saved
    )

    $current = $null
    try {
        $current = Get-VMetricSavedContext
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        Write-Verbose "Reading the saved sign-in again: $($_.Exception.Message)"
    }
    if ($current -and -not (Test-VMetricSameSecret -Left $current.Secret -Right $Saved.Secret)) {
        Write-Verbose 'The vault holds a newer saved sign-in; it is kept.'
        return
    }
    Remove-VMetricSavedSecret -Vault $Saved.Vault
    Remove-VMetricSavedContextMarker
}

function Invoke-VMetricSavedGrantRevoke {
    # POST /cli/grants/revoke for a saved sign-in: signed out (its sessions end with it), or with -Replaced only
    # retired (a newer one took its place; the sessions it minted run on).
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that call it ask ShouldProcess.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [pscustomobject] $Saved,

        [switch] $Replaced
    )

    $body = [ordered]@{ grant = $Saved.Secret }
    if ($Replaced) {
        $body['replaced'] = $true
    }
    $null = Invoke-VMetricRequest -Method POST -Path '/cli/grants/revoke' -Anonymous -ApiUrl $Saved.ApiUrl `
        -SkipCertificateCheck:([bool]$Saved.SkipCertificateCheck) -NoConflictRetry -Body $body
}

function Save-VMetricSavedContext {
    # Saves a session's sign-in for new windows: a saved sign-in minted for it (POST /cli/grants), or its API
    # token, in the default vault, and the marker. The one it replaces is retired on the server and removed.
    # Answers the vault's name.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'The public cmdlets that call it ask ShouldProcess.')]
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Session
    )

    $vault = Get-VMetricSaveVault
    $previous = $null
    try {
        $previous = Get-VMetricSavedContext
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        Write-Verbose "The sign-in saved before could not be read, so it is not retired on the server: $($_.Exception.Message)"
    }

    # What tells this save from any other, in the vault and in the marker: Test-VMetricSessionPersisted
    # compares it with the session's.
    $savedId = [guid]::NewGuid().ToString('N')
    $data = [ordered]@{
        version              = $script:VMetricSavedContextVersion
        savedId              = $savedId
        authMethod           = 'SavedSignIn'
        apiUrl               = [string]$Session.ApiUrl
        consoleUrl           = [string]$Session.ConsoleUrl
        skipCertificateCheck = [bool]$Session.SkipCertificateCheck
        secret               = $null
        grantId              = ''
        expiresAt            = 0
        tenantId             = [string]$Session.TenantId
        tenantName           = [string]$Session.TenantName
        email                = [string]$Session.Email
    }
    $minted = $null
    if ($Session.AuthMethod -eq 'ApiToken') {
        $data.authMethod = 'ApiToken'
        $data.secret = $Session.AccessToken
    }
    else {
        try {
            $answer = Invoke-VMetricRequest -Method POST -Path '/cli/grants' -Session $Session -NoConflictRetry
        }
        catch {
            if (Test-VMetricFlowControl -ErrorRecord $_) {
                throw
            }
            $inner = $_.Exception
            if ($inner -is [VMetricException] -and $inner.Code -eq 'CLI_GRANT_NOT_ALLOWED') {
                throw (New-VMetricException -Message 'This session started from the saved sign-in, which a saved sign-in cannot save again. To keep another tenant or account for new windows, sign in with Connect-VMetric -Persist.' `
                        -Code 'CLI_GRANT_NOT_ALLOWED' -Category PermissionDenied -StatusCode $inner.StatusCode -TraceId $inner.TraceId -InnerException $inner)
            }
            throw
        }
        $grant = [string](Get-VMetricMember -InputObject $answer -Name 'grant')
        if (-not $grant) {
            throw (New-VMetricException -Message 'The server answered no saved sign-in.' -Code 'InvalidGrantResponse' -Category InvalidResult)
        }
        $data.secret = ConvertTo-VMetricSecureString -String $grant
        $grant = $null
        $data.grantId = [string](Get-VMetricMember -InputObject $answer -Name 'id')
        $data.expiresAt = [long](Get-VMetricMember -InputObject $answer -Name 'expiresAt')
        $minted = [pscustomobject]@{ ApiUrl = $Session.ApiUrl; SkipCertificateCheck = [bool]$Session.SkipCertificateCheck; Secret = $data.secret }
    }

    try {
        $text = ConvertTo-VMetricJson -InputObject $data
        $secure = ConvertTo-VMetricSecureString -String $text
        $text = $null
        Write-VMetricSavedSecret -Vault $vault -Secret $secure
        $secure.Dispose()
        Write-VMetricSavedContextMarker -Vault $vault -SavedId $savedId
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        $inner = $_.Exception
        # Nothing holds the saved sign-in just minted: sign it out rather than leave it live.
        if ($minted) {
            try {
                Invoke-VMetricSavedGrantRevoke -Saved $minted
            }
            catch {
                Write-Verbose "Signing out the saved sign-in that could not be kept: $($_.Exception.Message)"
            }
        }
        throw (New-VMetricException -Message "Could not keep the sign-in in vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $inner.Message)" `
                -Code 'SavedContextNotWritten' -Category WriteError -InnerException $inner)
    }

    if ($previous) {
        if ($previous.Vault -ne $vault) {
            try {
                Remove-VMetricSavedSecret -Vault $previous.Vault
            }
            catch {
                Write-Warning "The sign-in saved before in vault '$(ConvertTo-VMetricSafeText -Text $previous.Vault)' could not be removed: $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)"
            }
        }
        if ($previous.AuthMethod -eq 'SavedSignIn' -and -not (Test-VMetricSameSecret -Left $previous.Secret -Right $data.secret)) {
            try {
                Invoke-VMetricSavedGrantRevoke -Saved $previous -Replaced
            }
            catch {
                if (Test-VMetricFlowControl -ErrorRecord $_) {
                    throw
                }
                Write-Warning "The sign-in saved before could not be retired on the server ($($_.Exception.Message)); it ends on its own, or revoke it in the console's list of PowerShell sessions."
            }
        }
    }

    $Session.SavedId = $savedId
    $vault
}

function Import-VMetricSavedContext {
    # The loader: in a window that has not signed in, the first call signs in with the saved sign-in and answers
    # the session; $null when nothing is saved or it cannot be used, said in a warning. A saved sign-in the
    # server refuses is removed; one that fails for another reason is kept, and the next call tries again.
    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    if ($script:VMetricSavedContextState -ne 'Pending') {
        return $null
    }
    # A window that signed in, or holds a session, is not signed in again behind its back.
    if ($script:VMetricSession -or $script:VMetricSessions.Count -gt 0) {
        $script:VMetricSavedContextState = 'Done'
        return $null
    }

    $attempt = 0
    while ($true) {
        $attempt++
        try {
            $saved = Get-VMetricSavedContext
        }
        catch {
            if (Test-VMetricFlowControl -ErrorRecord $_) {
                throw
            }
            $script:VMetricSavedContextState = 'Done'
            Write-Warning "The saved sign-in could not be used: $($_.Exception.Message)"
            return $null
        }
        if (-not $saved) {
            $script:VMetricSavedContextState = 'Done'
            return $null
        }
        if ($saved.AuthMethod -eq 'SavedSignIn' -and $saved.ExpiresAt -gt 0 -and
            [System.DateTimeOffset]::FromUnixTimeSeconds($saved.ExpiresAt) -le [System.DateTimeOffset]::UtcNow) {
            Write-Verbose 'The saved sign-in has ended; it is removed.'
            Remove-VMetricSavedContextCopy -Saved $saved
            $script:VMetricSavedContextState = 'Done'
            return $null
        }

        if ($saved.SkipCertificateCheck) {
            Write-Warning 'Certificate checks are off for the saved sign-in: the server''s identity is not verified, so anyone between you and it could read your tokens. Use this only with a test server.'
        }
        $address = @{ ApiUrl = $saved.ApiUrl; ConsoleUrl = $saved.ConsoleUrl; SkipCertificateCheck = $saved.SkipCertificateCheck }
        try {
            if ($saved.AuthMethod -eq 'ApiToken') {
                $session = New-VMetricSession @address -AuthMethod ApiToken -ApiToken $saved.Secret
                Initialize-VMetricApiTokenSession -Session $session
            }
            else {
                $answer = Invoke-VMetricTokenRequest -ApiUrl $saved.ApiUrl -SkipCertificateCheck:([bool]$saved.SkipCertificateCheck) -Body ([ordered]@{
                        grantType = 'stored_grant'
                        grant     = $saved.Secret
                    })
                $session = New-VMetricSession @address -AuthMethod SavedSignIn -TokenResponse $answer
            }
        }
        catch {
            if (Test-VMetricFlowControl -ErrorRecord $_) {
                throw
            }
            $failure = $_.Exception
            $code = ''
            $status = 0
            if ($failure -is [VMetricException]) {
                $code = $failure.Code
                $status = $failure.StatusCode
            }
            if ($code -in @('invalid_grant', 'access_denied', 'ApiTokenRefused')) {
                # Revoked, ended, or no longer the user's to use. A newer one saved meanwhile by another window
                # is tried once; this one is removed.
                $current = $null
                try {
                    $current = Get-VMetricSavedContext
                }
                catch {
                    Write-Verbose "Reading the saved sign-in again: $($_.Exception.Message)"
                }
                if ($attempt -eq 1 -and $current -and -not (Test-VMetricSameSecret -Left $current.Secret -Right $saved.Secret)) {
                    Write-Verbose 'Another window saved a newer sign-in meanwhile; using it.'
                    continue
                }
                try {
                    Remove-VMetricSavedContextCopy -Saved $saved
                }
                catch {
                    Write-Verbose "Removing the saved sign-in: $($_.Exception.Message)"
                }
                $script:VMetricSavedContextState = 'Done'
                Write-Warning "The saved sign-in can no longer be used ($($failure.Message)); it was removed. Run Connect-VMetric -Persist to sign in and keep the sign-in again."
                return $null
            }
            if ($code -eq 'ConnectionFailed' -or $status -ge 500 -or $status -eq 429) {
                Write-Warning "Could not sign in with the saved sign-in ($($failure.Message)); the next command tries again."
                return $null
            }
            $script:VMetricSavedContextState = 'Done'
            Write-Warning "Could not sign in with the saved sign-in: $($failure.Message)"
            return $null
        }

        $session.SavedId = $saved.SavedId
        $script:VMetricSavedContextState = 'Done'
        Register-VMetricSession -Session $session
        $account = if ($session.AuthMethod -eq 'ApiToken') { 'an API token' } else { ConvertTo-VMetricSafeText -Text ([string]$session.Email) }
        $tenantText = if ($session.TenantName) { " in tenant '$(ConvertTo-VMetricSafeText -Text ([string]$session.TenantName))'" } else { '' }
        Write-Verbose "Signed in to $($session.ConsoleUrl) as $account$tenantText with the sign-in saved in vault '$(ConvertTo-VMetricSafeText -Text $saved.Vault)'."
        return $session
    }
}

function Remove-VMetricSavedContext {
    # Disconnect-VMetric's part: signs the saved sign-in out on the server, which ends every session it minted
    # (an API token stays valid), and removes it from its vault and the marker. A sign-out the server does not
    # take is a warning: the local copy goes anyway. Answers whether a sign-in was saved.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Disconnect-VMetric asks ShouldProcess.')]
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    $vault = Read-VMetricSavedContextMarker
    if (-not $vault) {
        return $false
    }
    $saved = $null
    $unreadable = $false
    try {
        # $null when the vault holds none any more: the marker was all that was left, and is gone.
        $saved = Get-VMetricSavedContext
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        $unreadable = $true
        Write-Warning "The saved sign-in could not be read to sign it out on the server ($($_.Exception.Message)): revoke it in the console's list of PowerShell sessions."
    }
    if ($saved -and $saved.AuthMethod -eq 'SavedSignIn') {
        try {
            Invoke-VMetricSavedGrantRevoke -Saved $saved
        }
        catch {
            if (Test-VMetricFlowControl -ErrorRecord $_) {
                throw
            }
            Write-Warning "Could not sign out the saved sign-in on the server ($($_.Exception.Message)); it ends on its own, or revoke it in the console's list of PowerShell sessions."
        }
    }
    elseif ($saved) {
        Write-Verbose 'The saved API token is removed from the vault; revoke the token itself in the console.'
    }
    try {
        # What was read is removed, and what could not be read is tried; a vault that holds nothing
        # any more is left alone.
        if ($saved -or ($unreadable -and (Test-VMetricSecretManagement))) {
            Remove-VMetricSavedSecret -Vault $vault
        }
    }
    catch {
        if (Test-VMetricFlowControl -ErrorRecord $_) {
            throw
        }
        Write-Warning "The saved sign-in could not be removed from vault '$(ConvertTo-VMetricSafeText -Text $vault)': $(ConvertTo-VMetricSafeText -Text $_.Exception.Message)"
    }
    Remove-VMetricSavedContextMarker
    return $true
}