mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-08-12 21:53:05 -06:00
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user