Skip to content

Instantly share code, notes, and snippets.

@paxbun
Created July 18, 2025 05:47
Show Gist options
  • Save paxbun/16a7e6d13b96b93aeeccc4a97f053449 to your computer and use it in GitHub Desktop.
Save paxbun/16a7e6d13b96b93aeeccc4a97f053449 to your computer and use it in GitHub Desktop.
Generate AppStore Connect API JWT token
param (
[Parameter(Mandatory=$true)] [string] $ApiKey,
[Parameter(Mandatory=$true)] [string] $ApiIssuer,
[string] $KeyFile
)
function To-Base64Url([byte[]] $Bytes) {
return [Convert]::ToBase64String($Bytes).Replace("+","-").Replace("/","_").TrimEnd("=");
}
if (-not $KeyFile) {
$searchDirs = @();
if ($env:API_PRIVATE_KEYS_DIR) {
$searchDirs += $env:API_PRIVATE_KEYS_DIR;
}
$searchDirs += @(
"./private_keys"
Join-Path $env:HOME "private_keys"
Join-Path $env:HOME ".private_keys"
Join-Path $env:HOME ".appstoreconnect/private_keys"
);
foreach ($dir in $searchDirs) {
$candidate = Join-Path $dir "AuthKey_$ApiKey.p8";
if (Test-Path $candidate) {
$KeyFile = $candidate;
break;
}
}
if (-not $KeyFile) {
Write-Error "Could not find AuthKey_$ApiKey.p8 in any of: $($searchDirs -join ', ')";
exit 1;
}
}
# exp = now + 20 minutes
$expiry = [int]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + 1200);
$header = @{ alg = "ES256"; kid = $ApiKey } | ConvertTo-Json -Compress;
$payload = @{ iss = $ApiIssuer; exp = $expiry; aud = "appstoreconnect-v1" } | ConvertTo-Json -Compress;
$headerB64 = To-Base64Url([Text.Encoding]::UTF8.GetBytes($header));
$payloadB64 = To-Base64Url([Text.Encoding]::UTF8.GetBytes($payload));
$data = "$headerB64.$payloadB64";
$keyPem = Get-Content $KeyFile -Raw
$key = [System.Security.Cryptography.ECDsa]::Create();
$key.ImportFromPem($keyPem);
$dataBytes = [Text.Encoding]::ASCII.GetBytes($data);
$signature = $key.SignData($dataBytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256);
$signatureB64 = To-Base64Url($signature);
$jwt = "$data.$signatureB64";
return $jwt;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment