Providers/Okta/Private/Import-OktaAppCredential.ps1

function Import-OktaAppCredential {
    <#
    .SYNOPSIS
        Reads back the service app credential written by Export-OktaAppCredential

    .DESCRIPTION
        Resolves whichever storage mode the file records and returns a uniform object with the
        private JWK attached, so callers never branch on storage. Import-TestCredentialRecord
        reads a version 2 record and follows it to wherever the key is.

        Schema version 1 is still accepted, here rather than in Core because no other provider
        ever wrote one. Those files hold the JWK as plain JSON, from before the credential was
        encrypted at rest, and refusing them would strand anybody who seeded an environment with
        an earlier build. They are read, used, and reported with a warning naming the command
        that upgrades them - a warning being the right level because the credential still works
        and the run should not stop for it.

    .PARAMETER Path
        The credential file to read.

    .PARAMETER VaultPassword
        Password used to unlock the SecretStore vault, when the credential lives in one.

    .OUTPUTS
        PSCustomObject with orgUrl, clientId, appId, label, scopes, protection and privateJwk.

    .EXAMPLE
        PS> $credential = Import-OktaAppCredential -Path $path

        DESCRIPTION: Reads the record and recovers the private key
        OUTPUT: The credential with its JWK
        USE CASE: Connect-OktaEnvironment -ServiceApp and every token request

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter()]
        [System.Security.SecureString]$VaultPassword
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        throw ("No service app credential at '$Path'. Run New-OktaServiceApp while " +
            'connected with an SSWS token to create one.')
    }

    # A version 1 record carries the key as a JSON object under privateJwk and no protection
    # field. It is the one shape the shared reader does not know, so it is read here.
    $peek = ([System.Text.Encoding]::UTF8.GetString([System.IO.File]::ReadAllBytes($Path))).TrimStart([char]0xFEFF)
    try { $peek = $peek | ConvertFrom-Json } catch { throw "'$Path' is not valid JSON: $($_.Exception.Message)" }
    $legacy = (-not $peek.PSObject.Properties['protection'] -or -not $peek.protection) -and
        $peek.PSObject.Properties['privateJwk'] -and $peek.privateJwk

    if ($legacy) {
        foreach ($required in @('orgUrl', 'clientId', 'appId')) {
            if (-not $peek.PSObject.Properties[$required] -or -not $peek.$required) {
                throw "'$Path' is missing the required '$required' field and cannot be used."
            }
        }
        Write-Warning ("'$Path' stores the private key unencrypted, in the format used before this module " +
            'encrypted credentials at rest. Re-run New-OktaServiceApp -Force with an SSWS token to replace it.')
        $record = $peek
        $privateJwk = $peek.privateJwk
        $protection = 'None'
    }
    else {
        $read = Import-TestCredentialRecord -Path $Path -Required orgUrl, clientId, appId -SecretField 'privateJwkProtected' `
            -SecretLabel 'key' -MissingRecordMessage 'Run New-OktaServiceApp while connected with an SSWS token to create one.' `
            -VaultPassword $VaultPassword
        $record = $read.Record
        $protection = $read.Protection
        try { $privateJwk = $read.Secret | ConvertFrom-Json }
        catch { throw "The stored private key in '$Path' is not valid JSON: $($_.Exception.Message)" }
    }

    return [PSCustomObject]@{
        orgUrl     = $record.orgUrl
        clientId   = $record.clientId
        appId      = $record.appId
        label      = $record.label
        scopes     = @($record.scopes)
        protection = $protection
        vaultName  = $(if ($record.PSObject.Properties['vaultName']) { $record.vaultName } else { $null })
        secretName = $(if ($record.PSObject.Properties['secretName']) { $record.secretName } else { $null })
        createdUtc = $(if ($record.PSObject.Properties['createdUtc']) { $record.createdUtc } else { $null })
        privateJwk = $privateJwk
        path       = $Path
    }
}