Update Lenovo PSREF authentication

Replace the obsolete cookie with signed bearer-token requests for Lenovo model searches in both the UI and CLI. Surface PSREF failures instead of reporting empty results.
This commit is contained in:
rbalsleyMSFT
2026-08-11 19:07:59 -07:00
parent 7a3501e40c
commit 16d466a9c3
3 changed files with 171 additions and 51 deletions
+17 -26
View File
@@ -1531,39 +1531,30 @@ function Get-LenovoDrivers {
[string]$ModelName
)
# Lenovo is special - they prevent access to the PSREF API without a cookie as of July 2025.
# This cookie must be retrieved via Javascript
# It appears that the cookie is hard-coded. We'll see how long this lasts.
# If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know.
# https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models).
$lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg"
# Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes.
# 3/25/2026 - The cookie is still the same after 8 months, but we'll keep the retrieval function in case it changes in the future or if we need to get a new one.
# $lenovoCookie = Get-LenovoPSREFToken
# Add the cookie to the headers
$Headers["Cookie"] = $lenovoCookie
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$ModelName"
WriteLog "Querying Lenovo PSREF API for model: $ModelName"
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent
$VerbosePreference = $OriginalVerbosePreference
WriteLog "Complete"
$jsonResponse = $response.Content | ConvertFrom-Json
$normalizedModelName = $ModelName.Trim()
$searchType = if ($normalizedModelName.Length -eq 4) { 'MT' } elseif ($normalizedModelName.Length -eq 10) { 'Model' } else { 'Normal' }
$cacheBuster = ([System.Random]::new().NextDouble()).ToString('0.################', [Globalization.CultureInfo]::InvariantCulture)
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($normalizedModelName))&limit=6&IsPreviewProduct=true&SearchType=$searchType&t=$cacheBuster"
WriteLog "Querying Lenovo PSREF API for model: $normalizedModelName"
$requestHeaders = Get-LenovoPSREFRequestHeaders -Method GET -Uri $url -Headers $Headers -UserAgent $UserAgent
$originalVerbosePreference = $VerbosePreference
try {
$VerbosePreference = 'SilentlyContinue'
$jsonResponse = Invoke-RestMethod -Uri $url -Headers $requestHeaders -UserAgent $UserAgent -ErrorAction Stop
}
finally {
$VerbosePreference = $originalVerbosePreference
}
WriteLog "Lenovo PSREF API query complete."
$products = @()
foreach ($item in $jsonResponse.data) {
if (-not [string]::IsNullOrEmpty($item.MachineType) -and -not [string]::IsNullOrEmpty($item.ProductName)) {
$productName = $item.ProductName
$machineTypes = $item.MachineType -split " / "
$machineTypes = $item.MachineType -split '\s*/\s*'
foreach ($machineType in $machineTypes) {
if ($machineType -eq $ModelName) {
if ($machineType -eq $normalizedModelName) {
WriteLog "Model name entered is a matching machine type"
$products = @()
$products += [pscustomobject]@{
@@ -457,6 +457,143 @@ function Test-ExistingDriver {
# If neither WIM nor a valid folder exists, return null
return $null
}
$script:LenovoPSREFAuthContext = $null
function Get-LenovoPSREFRequestHeaders {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH')]
[string]$Method,
[Parameter(Mandatory = $true)]
[uri]$Uri,
[Parameter()]
[AllowEmptyString()]
[string]$Body = '',
[Parameter()]
[hashtable]$Headers = @{},
[Parameter()]
[string]$UserAgent
)
$currentUnixTime = [long][Math]::Floor(([DateTime]::UtcNow - [DateTime]'1970-01-01').TotalSeconds)
if ($null -eq $script:LenovoPSREFAuthContext -or
$script:LenovoPSREFAuthContext.ExpiresAt - $currentUnixTime -le 120 -or
$script:LenovoPSREFAuthContext.UserAgent -ne $UserAgent) {
$authRequestParameters = @{
Uri = 'https://psref.lenovo.com/api/home/auth/issue'
Method = 'Post'
ContentType = 'application/json'
Body = '{}'
Headers = @{
Accept = 'application/json, text/plain, */*'
Origin = 'https://psref.lenovo.com'
Referer = 'https://psref.lenovo.com/'
'Sec-Fetch-Dest' = 'empty'
'Sec-Fetch-Mode' = 'cors'
'Sec-Fetch-Site' = 'same-origin'
}
ErrorAction = 'Stop'
}
if (-not [string]::IsNullOrWhiteSpace($UserAgent)) {
$authRequestParameters.UserAgent = $UserAgent
}
WriteLog 'Requesting a Lenovo PSREF API access token.'
$authResponse = Invoke-RestMethod @authRequestParameters
$accessToken = [string]$authResponse.access_token
if ([string]::IsNullOrWhiteSpace($accessToken)) {
throw 'Lenovo PSREF authentication returned an empty access token.'
}
$tokenParts = $accessToken.Split('.')
if ($tokenParts.Count -ne 3) {
throw 'Lenovo PSREF authentication returned an invalid access token.'
}
$payloadSegment = $tokenParts[1].Replace('-', '+').Replace('_', '/')
switch ($payloadSegment.Length % 4) {
2 { $payloadSegment += '==' }
3 { $payloadSegment += '=' }
1 { throw 'Lenovo PSREF authentication returned an invalid token payload.' }
}
try {
$payloadBytes = [Convert]::FromBase64String($payloadSegment)
$tokenPayload = [Text.Encoding]::UTF8.GetString($payloadBytes) | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "Unable to decode the Lenovo PSREF access token: $($_.Exception.Message)"
}
$signatureSecret = [string]$tokenPayload.ss
$expiresAt = [long]$tokenPayload.exp
if ([string]::IsNullOrWhiteSpace($signatureSecret) -or $signatureSecret -notmatch '^(?:[0-9a-fA-F]{2})+$') {
throw 'Lenovo PSREF authentication returned an invalid signing secret.'
}
if ($expiresAt -le $currentUnixTime) {
throw 'Lenovo PSREF authentication returned an expired access token.'
}
$script:LenovoPSREFAuthContext = [PSCustomObject]@{
AccessToken = $accessToken
SignatureSecret = $signatureSecret
ExpiresAt = $expiresAt
UserAgent = $UserAgent
}
WriteLog 'Lenovo PSREF API access token acquired.'
}
$requestHeaders = @{}
foreach ($key in $Headers.Keys) {
if ($key -notmatch '^(Accept|Authorization|Cookie|Priority|Sec-|Upgrade-Insecure-Requests|X-)') {
$requestHeaders[$key] = $Headers[$key]
}
}
$requestHeaders.Accept = 'application/json, text/plain, */*'
$requestHeaders.Referer = 'https://psref.lenovo.com/'
$requestHeaders['Sec-Fetch-Dest'] = 'empty'
$requestHeaders['Sec-Fetch-Mode'] = 'cors'
$requestHeaders['Sec-Fetch-Site'] = 'same-origin'
$bodyHash = ''
if (-not [string]::IsNullOrEmpty($Body)) {
$sha256 = [Security.Cryptography.SHA256]::Create()
try {
$bodyHashBytes = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($Body))
$bodyHash = -join ($bodyHashBytes | ForEach-Object { $_.ToString('x2') })
}
finally {
$sha256.Dispose()
}
}
$timestamp = [long][Math]::Floor(([DateTime]::UtcNow - [DateTime]'1970-01-01').TotalSeconds)
$nonce = [guid]::NewGuid().ToString()
$canonicalRequest = "$($Method.ToUpperInvariant())|$($Uri.PathAndQuery)|$bodyHash|$timestamp|$nonce"
$signatureSecret = $script:LenovoPSREFAuthContext.SignatureSecret
$secretBytes = New-Object byte[] ($signatureSecret.Length / 2)
for ($characterIndex = 0; $characterIndex -lt $signatureSecret.Length; $characterIndex += 2) {
$secretBytes[$characterIndex / 2] = [Convert]::ToByte($signatureSecret.Substring($characterIndex, 2), 16)
}
$hmac = [Security.Cryptography.HMACSHA256]::new($secretBytes)
try {
$signatureBytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalRequest))
$signature = -join ($signatureBytes | ForEach-Object { $_.ToString('x2') })
}
finally {
$hmac.Dispose()
}
$requestHeaders.Authorization = "Bearer $($script:LenovoPSREFAuthContext.AccessToken)"
$requestHeaders['X-Ts'] = $timestamp.ToString()
$requestHeaders['X-Nonce'] = $nonce
$requestHeaders['X-Sig'] = $signature
return $requestHeaders
}
function Get-LenovoPSREFToken {
<#
@@ -819,4 +956,5 @@ Export-ModuleMember -Function `
Compress-DriverFolderToWim, `
Update-DriverMappingJson, `
Test-ExistingDriver, `
Get-LenovoPSREFRequestHeaders, `
Get-LenovoPSREFToken
@@ -17,38 +17,29 @@ function Get-LenovoDriversModelList {
[string]$UserAgent
)
# Lenovo is special - they prevent access to the PSREF API without a cookie as of July 2025.
# This cookie must be retrieved via Javascript
# It appears that the cookie is hard-coded. We'll see how long this lasts.
# If anyone knows how to reliably get the the model and machine type information from Lenovo, let me know.
# https://download.lenovo.com/cdrt/td/catalogv2.xml only provides a subset of the information available from PSREF (e.g. it's missing 300w, 500w, and other consumer models).
$lenovoCookie = "X-PSREF-USER-TOKEN=eyJ0eXAiOiJKV1QifQ.bjVTdWk0YklZeUc2WnFzL0lXU0pTeU1JcFo0aExzRXl1UGxHN3lnS1BtckI0ZVU5WEJyVGkvaFE0NmVNU2U1ZjNrK3ZqTEVIZ29nTk1TNS9DQmIwQ0pTN1Q1VytlY1RpNzZTUldXbm4wZ1g2RGJuQWg4MXRkTmxKT2YrOW9LRjBzQUZzV05HM3NpcU92WFVTM0o0blM1SDQyUlVXNThIV1VBS2R0c1B2NjJyQjIrUGxNZ2x6RTRhUjY5UDZWclBX.ZDBmM2EyMWRjZTg2N2JmYWMxZDIxY2NiYjQzMWFhNjg1YjEzZTAxNmU2M2RmN2M5ZjIyZWJhMzZkOWI1OWJhZg"
# Wrote a separate function to grab the token. Check the function notes for more details. Keep the above comment for now to see if the cookie ever changes.
# 3/25/2026 - The cookie is still the same after 8 months, but we'll keep the retrieval function in case it changes in the future or if we need to get a new one.
# $lenovoCookie = Get-LenovoPSREFToken
# Add the cookie to the headers
$Headers["Cookie"] = $lenovoCookie
WriteLog "Querying Lenovo PSREF API for model/machine type: $ModelSearchTerm"
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($ModelSearchTerm))"
$normalizedSearchTerm = $ModelSearchTerm.Trim()
$searchType = if ($normalizedSearchTerm.Length -eq 4) { 'MT' } elseif ($normalizedSearchTerm.Length -eq 10) { 'Model' } else { 'Normal' }
$cacheBuster = ([System.Random]::new().NextDouble()).ToString('0.################', [Globalization.CultureInfo]::InvariantCulture)
$url = "https://psref.lenovo.com/api/search/DefinitionFilterAndSearch/Suggest?kw=$([uri]::EscapeDataString($normalizedSearchTerm))&limit=6&IsPreviewProduct=true&SearchType=$searchType&t=$cacheBuster"
$models = [System.Collections.Generic.List[PSCustomObject]]::new()
try {
$OriginalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent -ErrorAction Stop
$VerbosePreference = $OriginalVerbosePreference
WriteLog "Querying Lenovo PSREF API for model/machine type: $normalizedSearchTerm"
$requestHeaders = Get-LenovoPSREFRequestHeaders -Method GET -Uri $url -Headers $Headers -UserAgent $UserAgent
$originalVerbosePreference = $VerbosePreference
try {
$VerbosePreference = 'SilentlyContinue'
$jsonResponse = Invoke-RestMethod -Uri $url -Headers $requestHeaders -UserAgent $UserAgent -ErrorAction Stop
}
finally {
$VerbosePreference = $originalVerbosePreference
}
WriteLog "PSREF API query complete."
$jsonResponse = $response.Content | ConvertFrom-Json
if ($null -ne $jsonResponse.data -and $jsonResponse.data.Count -gt 0) {
foreach ($item in $jsonResponse.data) {
$productName = $item.ProductName
$machineTypes = $item.MachineType -split " / " # Split if multiple machine types are listed
$machineTypes = $item.MachineType -split '\s*/\s*' # Split if multiple machine types are listed
foreach ($machineTypeRaw in $machineTypes) {
$machineType = $machineTypeRaw.Trim()
@@ -77,7 +68,7 @@ function Get-LenovoDriversModelList {
}
catch {
WriteLog "Error querying Lenovo PSREF API: $($_.Exception.Message)"
# Return empty list on error
throw "Lenovo PSREF model search failed: $($_.Exception.Message)"
}
return $models
}