Providers/Okta/Private/Export-OktaAppCredential.ps1
|
function Export-OktaAppCredential { <# .SYNOPSIS Writes the service app's client id and private key to protected storage .DESCRIPTION This is the handover point between the two authentication modes: everything before it runs on the SSWS token you pasted in, everything after it can run on the app. Three storage modes, and the metadata file is written in all of them so that the org-to-credential mapping is always discoverable in one place: - DPAPI (default). The key is encrypted into the file with a key derived from your user account and this machine. No modules, nothing to remember, and the file is useless if it is copied elsewhere. - SecretStore. The key goes into an encrypted vault and the file holds only a pointer to it. This is the cross-platform option, and the one to use if you already keep other lab credentials in a vault. - None. Only reached when DPAPI is unavailable, which today means a non-Windows host without -UseSecretStore. It warns rather than failing, because an app registered in Okta with no key on disk to match it is worse than a key with a warning attached. The file is written first and its ACL restricted second, which is the wrong order in the abstract because Set-Acl needs the file to exist. The gap is closed by creating the parent folder with a restricted ACL beforehand, so the file is never reachable by another user even during it. Under DPAPI the point is close to moot anyway - the bytes on disk are ciphertext either way. .PARAMETER Path Destination file .PARAMETER OrgUrl Org the credential belongs to .PARAMETER ClientId The service app client_id .PARAMETER AppId The app instance id, which the management API uses for grants and lifecycle .PARAMETER Label The app label, so teardown can find the app without this file .PARAMETER Scopes The Okta API scopes granted to the app .PARAMETER PrivateJwk The private JWK to store .PARAMETER UseSecretStore Store the key in a SecretStore vault instead of encrypting it into the file .PARAMETER VaultName Vault to use when -UseSecretStore is specified .PARAMETER VaultPassword Password for the vault when -UseSecretStore is specified .OUTPUTS PSCustomObject describing what was written .EXAMPLE Export-OktaAppCredential -Path $path -OrgUrl $org -ClientId $id -AppId $appId ` -Label 'OKTALAB Automation' -Scopes $scopes -PrivateJwk $jwk .NOTES Author: Jeffrey Stuhr Version: 2.0.0 Last Updated: 2026-08-07 #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Path, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$OrgUrl, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$ClientId, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$AppId, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$Label, [Parameter(Mandatory = $true)][string[]]$Scopes, [Parameter(Mandatory = $true)][object]$PrivateJwk, [Parameter()][switch]$UseSecretStore, [Parameter()][string]$VaultName = 'OktaEnvironment', [Parameter()][System.Security.SecureString]$VaultPassword ) if (-not $PSCmdlet.ShouldProcess($Path, 'Write the service app credential')) { return $null } $folder = Split-Path -Path $Path -Parent if ($folder -and -not (Test-Path -Path $folder)) { $null = New-Item -Path $folder -ItemType Directory -Force # Lock the folder before anything sensitive lands in it, so the file is never # world-readable even for the instant between New-Item and Set-Acl below. $null = Protect-OktaFile -Path $folder -Confirm:$false } $jwkJson = $PrivateJwk | ConvertTo-Json -Depth 10 -Compress $payload = [ordered]@{ schemaVersion = 2 orgUrl = $OrgUrl.TrimEnd('/') clientId = $ClientId appId = $AppId label = $Label scopes = @($Scopes) createdUtc = [DateTime]::UtcNow.ToString('o') } $secretName = 'OktaEnvironment-{0}-{1}' -f ([uri]$OrgUrl).Host, $ClientId if ($UseSecretStore) { $vaultArgs = @{ VaultName = $VaultName; Install = $true; Confirm = $false } if ($VaultPassword) { $vaultArgs.VaultPassword = $VaultPassword } $null = Initialize-TestSecretVault @vaultArgs $null = Set-TestVaultSecret -VaultName $VaultName -SecretName $secretName ` -PlainText $jwkJson -Confirm:$false $payload.protection = 'SecretStore' $payload.vaultName = $VaultName $payload.secretName = $secretName } else { $protected = Protect-OktaSecret -PlainText $jwkJson $payload.protection = $protected.Method $payload.privateJwkProtected = $protected.Value } $json = $payload | ConvertTo-Json -Depth 10 # Set-Content -Encoding UTF8 writes a BOM on Windows PowerShell, which ConvertFrom-Json on # the same edition then chokes on. Writing the bytes avoids arguing with either. [System.IO.File]::WriteAllBytes($Path, [System.Text.Encoding]::UTF8.GetBytes($json)) $protectedFile = Protect-OktaFile -Path $Path -Confirm:$false return [PSCustomObject]@{ Path = $Path ClientId = $ClientId AppId = $AppId Scopes = @($Scopes) Protection = $payload.protection VaultName = if ($UseSecretStore) { $VaultName } else { $null } SecretName = if ($UseSecretStore) { $secretName } else { $null } FileProtected = $protectedFile } } |