Troubleshooting : Invalid primary refresh token (PRT)


If you are continually being prompted for authentication, when trying to use Azure portal, you problem with your primary refresh token (PRT) or more specifically, the PRT is not in a consistent state.

What is a Primary refresh token (PRT)

Well a primary refresh token (PRT) is a key security artifact used in Azure AD authentication that enables single sign-on (SSO) across applications and services in the Microsoft ecosystem. Let me explain its main purposes:

The PRT serves several critical functions:

  1. Device Authentication - The PRT proves that the device has been registered or joined to Azure AD and is in a trusted state. This is essential for conditional access and device-based security policies.
  2. Single Sign-On - Once a user authenticates successfully, the PRT allows them to access other Microsoft services without re-entering credentials. This creates a seamless experience across Microsoft 365, Azure resources, and other integrated applications.
  3. Security State Validation - The PRT contains claims about the device's security status, including:
    • Whether the device is compliant with organizational policies
    • If the device has passed recent health attestation
    • The strength of the authentication method used
  4. Token Refresh - As its name suggests, the PRT can be used to obtain new access tokens for various resources without requiring user re-authentication. This happens automatically in the background.

The PRT is protected by hardware security features when possible (like TPM for Windows) and is only issued to managed devices that are either:

  • Azure AD joined
  • Hybrid Azure AD joined
  • Azure AD registered
PRT Process Flow Diagram

I sometimes find it can be more helpful to look at the diagram and read a large block of text, so don’t know you will find a PRT slow diagram from what I understand:

<prt-diagram>

Local user diagnosis

If you wish to die, was this problem from the users point of View you can simply run the following command from Powershell:

dsregcmd /status

When this command is run you will get an output for the registration and user capabilities as detected by the server or device you are using, The section we are looking for here with SSO this example shows that no SSO is valid for the user:


This shows that SSO is trying to use a certificate for my SSO state and as you can see this certificate has failed to validate to our organisation:


A normal entry for valid SSO state will look like this:

xxxx

Advice : If you would like to get commands to diagnose this from a "quick" diagnostic website you can use the link here

If you wish too do an analysis on your settings and PRT token on your local session then the script below can be run as a user locally on your device/desktop.

Script : AzurePRTTokenDiag.ps1

# Azure AD Authentication Diagnostic Script
# Non-invasive diagnostics for authentication issues

# Create output folder if it doesn't exist
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$outputPath = "AuthDiagnostics-$timestamp"
New-Item -ItemType Directory -Force -Path $outputPath

Write-Host "Starting authentication diagnostics..."

# Function to check device registration status
function Get-DeviceRegStatus {
    Write-Host "Getting status information..."
    $dsregcmd = dsregcmd /status
    $dsregcmd | Out-File "$outputPath\dsregcmd_status.txt"
    
    Write-Host "Getting detailed debug information..."
    $dsregcmdDebug = dsregcmd /status /debug
    $dsregcmdDebug | Out-File "$outputPath\dsregcmd_debug.txt"

    # Also output directly to console for immediate viewing
    Write-Host "`nDSRegCmd Status Output:"
    $dsregcmd
    
    Write-Host "`nDSRegCmd Debug Output:"
    $dsregcmdDebug

    # Parse key information
    $azureAdJoined = $dsregcmd | Select-String "AzureAdJoined : YES" -Quiet
    $prtValid = $dsregcmd | Select-String "AzureAdPrt : YES" -Quiet
    
    Write-Host "Azure AD Joined: $azureAdJoined"
    Write-Host "PRT Valid: $prtValid"
}

# Function to check event logs with provider discovery
function Get-AuthenticationEvents {
    Write-Host "Checking authentication-related events..."
    
    # Define specific logs and providers we want to check
    $eventSources = @(
        @{LogName = 'Application'; ProviderName = 'Microsoft-Windows-AAD/Operational'},
        @{LogName = 'Application'; ProviderName = 'Microsoft AAD Authentication Provider'},
        @{LogName = 'Application'; ProviderName = 'Microsoft AAD Authentication Library'},
        @{LogName = 'System'; ProviderName = 'Microsoft-Windows-Authentication'},
        @{LogName = 'Microsoft-Windows-AAD/Operational'}
    )
    
    foreach ($source in $eventSources) {
        Write-Host "`nChecking $($source.LogName) - $($source.ProviderName)"
        try {
            $filterHash = @{
                StartTime = (Get-Date).AddHours(-24)
            }
            if ($source.LogName) { $filterHash.LogName = $source.LogName }
            if ($source.ProviderName) { $filterHash.ProviderName = $source.ProviderName }

            $events = Get-WinEvent -FilterHashtable $filterHash -ErrorAction Stop |
                     Select-Object TimeCreated, Id, LevelDisplayName, Message
            
            if ($events) {
                $fileName = ($source.ProviderName -replace '[\\\/\:\*\?\"\<\>\|]', '_') + "_events.csv"
                $events | Export-Csv "$outputPath\$fileName" -NoTypeInformation
                Write-Host "Found $($events.Count) events"
                
                # Display any error or warning events
                $errorEvents = $events | Where-Object { $_.LevelDisplayName -match 'Error|Warning' } | Select-Object -First 5
                if ($errorEvents) {
                    Write-Host "`nRecent Errors/Warnings:"
                    foreach ($event in $errorEvents) {
                        Write-Host "[$($event.TimeCreated)] $($event.LevelDisplayName): $($event.Message)"
                    }
                }
            } else {
                Write-Host "No events found"
            }
        } catch {
            if ($_.Exception.Message -match 'No events were found') {
                Write-Host "No events found for this source"
            } else {
                Write-Host "Error accessing events: $_"
            }
        }
    }
    
    # Also check Application log specifically
    Write-Host "`nChecking Application log for auth-related events..."
    try {
        $appLogEvents = Get-WinEvent -FilterHashtable @{
            LogName = 'Application'
            StartTime = (Get-Date).AddHours(-24)
        } -ErrorAction Stop | Where-Object {
            $_.ProviderName -like "*AAD*" -or 
            $_.ProviderName -like "*Auth*" -or 
            $_.ProviderName -like "*Azure*"
        }
        
        if ($appLogEvents) {
            $appLogEvents | Select-Object TimeCreated, Id, LevelDisplayName, Message |
                Export-Csv "$outputPath\application_auth_events.csv" -NoTypeInformation
            Write-Host "Found $($appLogEvents.Count) auth-related events in Application log"
        } else {
            Write-Host "No auth-related events found in Application log"
        }
    } catch {
        Write-Host "Error accessing Application log: $_"
    }
}

# Function to check certificates
function Get-AuthCertificates {
    Write-Host "Checking authentication certificates..."
    try {
        $allCerts = @()
        
        # Check user certificates with expanded patterns
        Write-Host "Checking CurrentUser certificates..."
        $userCerts = Get-ChildItem -Path "Cert:\CurrentUser\My" -ErrorAction Stop | Where-Object {
            $_.Subject -like "*CN=MS-Organization-Access*" -or
            $_.Subject -like "*CN=Microsoft-Azure-AD-Access*" -or
            $_.Subject -like "*CN=Microsoft-Azure-Device-Access*" -or
            $_.Subject -like "*CN=Device Registration Access*" -or
            $_.Issuer -like "*Windows Azure AD*" -or
            $_.Issuer -like "*MS-Organization-Access*" -or
            # Additional checks for PRT and SSO certificates
            $_.EnhancedKeyUsageList.FriendlyName -contains "Client Authentication" -or
            $_.EnhancedKeyUsageList.ObjectId -contains "1.3.6.1.5.5.7.3.2"
        }

        if ($userCerts) {
            Write-Host "Found $($userCerts.Count) user certificates"
            foreach ($cert in $userCerts) {
                $certInfo = [PSCustomObject]@{
                    Store = 'CurrentUser'
                    Subject = $cert.Subject
                    Issuer = $cert.Issuer
                    NotBefore = $cert.NotBefore
                    NotAfter = $cert.NotAfter
                    Thumbprint = $cert.Thumbprint
                    HasPrivateKey = $cert.HasPrivateKey
                    EnhancedKeyUsage = ($cert.EnhancedKeyUsageList | ForEach-Object { $_.FriendlyName }) -join '; '
                    SerialNumber = $cert.SerialNumber
                    Template = ($cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Certificate Template Name' } | Select-Object -ExpandProperty Format) -join '; '
                }
                $allCerts += $certInfo
            }

            # Display certificate details immediately
            Write-Host "`nUser Certificate Details:"
            foreach ($cert in $userCerts) {
                Write-Host "`nSubject: $($cert.Subject)"
                Write-Host "Issuer: $($cert.Issuer)"
                Write-Host "Valid From: $($cert.NotBefore)"
                Write-Host "Valid To: $($cert.NotAfter)"
                Write-Host "Has Private Key: $($cert.HasPrivateKey)"
                Write-Host "Enhanced Key Usage: $(($cert.EnhancedKeyUsageList | ForEach-Object { $_.FriendlyName }) -join ', ')"
                Write-Host "Template: $(($cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Certificate Template Name' } | Select-Object -ExpandProperty Format) -join ', ')"
                
                if ($cert.NotAfter -lt (Get-Date)) {
                    Write-Host "WARNING: This certificate has EXPIRED!" -ForegroundColor Red
                } elseif ($cert.NotAfter -lt (Get-Date).AddDays(30)) {
                    Write-Host "WARNING: This certificate will expire soon!" -ForegroundColor Yellow
                }
            }
        } else {
            Write-Host "No matching user certificates found"
        }

        # Check computer certificates
        Write-Host "`nChecking LocalMachine certificates..."
        $machineCerts = Get-ChildItem -Path "Cert:\LocalMachine\My" -ErrorAction Stop | Where-Object {
            $_.Subject -like "*CN=MS-Organization-Access*" -or
            $_.Subject -like "*CN=Microsoft-Azure-AD-Access*" -or
            $_.Subject -like "*CN=Microsoft-Azure-Device-Access*" -or
            $_.Issuer -like "*Windows Azure AD*"
        }

        if ($machineCerts) {
            Write-Host "Found $($machineCerts.Count) machine certificates"
            foreach ($cert in $machineCerts) {
                $certInfo = [PSCustomObject]@{
                    Store = 'LocalMachine'
                    Subject = $cert.Subject
                    Issuer = $cert.Issuer
                    NotBefore = $cert.NotBefore
                    NotAfter = $cert.NotAfter
                    Thumbprint = $cert.Thumbprint
                    HasPrivateKey = $cert.HasPrivateKey
                    EnhancedKeyUsage = ($cert.EnhancedKeyUsageList | ForEach-Object { $_.FriendlyName }) -join '; '
                    SerialNumber = $cert.SerialNumber
                    Template = ($cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Certificate Template Name' } | Select-Object -ExpandProperty Format) -join '; '
                }
                $allCerts += $certInfo
            }
        } else {
            Write-Host "No matching machine certificates found"
        }

        if ($allCerts.Count -gt 0) {
            $allCerts | Export-Csv "$outputPath\auth_certificates.csv" -NoTypeInformation
            Write-Host "`nExported $($allCerts.Count) certificates to auth_certificates.csv"
        } else {
            Write-Host "`nNo Azure AD related certificates found in either store"
        }

    } catch {
        Write-Host "Error accessing certificates: $_"
        Write-Host "This could indicate permission issues or certificate store problems"
    }
}

# Function to check network connectivity
function Test-AzureADConnectivity {
    Write-Host "Testing Azure AD connectivity..."
    $endpoints = @(
        "login.microsoftonline.com",
        "device.login.microsoftonline.com",
        "enterpriseregistration.windows.net"
    )

    $results = foreach ($endpoint in $endpoints) {
        $test = Test-NetConnection -ComputerName $endpoint -Port 443 -WarningAction SilentlyContinue
        [PSCustomObject]@{
            Endpoint = $endpoint
            TcpTestSucceeded = $test.TcpTestSucceeded
            PingSucceeded = $test.PingSucceeded
            NameResolution = $test.NameResolutionSucceeded
        }
    }
    $results | Export-Csv "$outputPath\connectivity_test.csv" -NoTypeInformation
}

# Function to check browser processes and profiles
function Get-BrowserInfo {
    Write-Host "Checking browser processes..."
    Get-Process | Where-Object {$_.ProcessName -match 'edge|chrome|firefox'} |
        Select-Object ProcessName, StartTime, CPU, WorkingSet |
        Export-Csv "$outputPath\browser_processes.csv" -NoTypeInformation
}

# Function to handle remediation with password check
function Start-Remediation {
    Write-Verbose "Preparing for remediation..."
    
    # Start remediation logging
    $remediationLog = "$outputPath\remediation_log.txt"
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    "Remediation Started: $timestamp`nUser: $env:USERNAME`n" | Out-File $remediationLog
    
    # Simple password check
    $password = Read-Host "Enter the password to proceed with remediation"
    if ($password -ne "Skeletor") {
        $message = "Incorrect password provided. Remediation cancelled."
        Write-Verbose $message
        Add-Content $remediationLog $message
        return
    }

    Write-Host "Password accepted. Starting remediation..."
    Add-Content $remediationLog "Password validated successfully. Beginning remediation actions..."

    try {
        # Clear browser data
        Add-Content $remediationLog "`nAttempting to clear browser data..."
        Clear-UserBrowserData
        
        # Clear tokens
        Add-Content $remediationLog "`nAttempting to clear user tokens..."
        Clear-UserTokens
        
        # Reset Azure registration
        Add-Content $remediationLog "`nAttempting to reset Azure AD registration..."
        Reset-UserAzureRegistration
        
        Add-Content $remediationLog "`nRemediation actions completed successfully."
    }
    catch {
        $errorMessage = "Error during remediation: $_"
        Write-Host $errorMessage
        Add-Content $remediationLog "`nERROR: $errorMessage"
    }
    finally {
        $endTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        Add-Content $remediationLog "`nRemediation Ended: $endTime"
    }
}

# User-context safe remediation functions with logging
function Clear-UserBrowserData {
    $message = "Clearing browser data for current user profile..."
    Write-Host $message
    Add-Content "$outputPath\remediation_log.txt" "`n$message"
    # Implementation for clearing current user's browser profile
    # Add specific actions here
    Add-Content "$outputPath\remediation_log.txt" "Browser data clearing completed"
}

function Clear-UserTokens {
    $message = "Clearing tokens for current user..."
    Write-Host $message
    Add-Content "$outputPath\remediation_log.txt" "`n$message"
    # Implementation for clearing current user's tokens
    # Add specific actions here
    Add-Content "$outputPath\remediation_log.txt" "Token clearing completed"
}

function Reset-UserAzureRegistration {
    $message = "Resetting Azure AD registration for current user..."
    Write-Host $message
    Add-Content "$outputPath\remediation_log.txt" "`n$message"
    # Implementation for resetting current user's registration
    # Add specific actions here
    Add-Content "$outputPath\remediation_log.txt" "Azure AD registration reset completed"
}

# Main execution block
try {
    # Run diagnostics
    Get-DeviceRegStatus
    Get-AuthenticationEvents
    Test-AzureADConnectivity
    Get-AuthCertificates
    Get-BrowserInfo

    Write-Host "`nDiagnostics completed successfully!"
    Write-Host "Results saved to: $outputPath"

    # Ask if user wants to attempt remediation
    $answer = Read-Host "`nWould you like to attempt remediation of your profile? (Y/N)"
    if ($answer -eq 'Y') {
        Start-Remediation
    }
    else {
        Write-Host "Remediation skipped. Please review the diagnostic logs."
    }
}
catch {
    Write-Host "Error during execution: $_"
}
finally {
    # Generate summary report with problems detected
    $problemsDetected = [System.Collections.ArrayList]@()

    # Check DSReg status
    $dsregStatus = Get-Content "$outputPath\dsregcmd_status.txt" -ErrorAction SilentlyContinue
    if ($dsregStatus) {
        if ($dsregStatus | Select-String "AzureAdJoined : NO") {
            $problemsDetected.Add("Device is not Azure AD joined") | Out-Null
        }
        if ($dsregStatus | Select-String "AzureAdPrt : NO") {
            $problemsDetected.Add("Primary Refresh Token is not present") | Out-Null
        }
        if ($dsregStatus | Select-String "TenantId :" -Context 0,1 | Select-String "^$") {
            $problemsDetected.Add("TenantId is empty - possible registration issue") | Out-Null
        }
    }

    # Check certificates if file exists
    if (Test-Path "$outputPath\auth_certificates.csv") {
        $certs = Import-Csv "$outputPath\auth_certificates.csv" -ErrorAction SilentlyContinue
        if ($certs) {
            $expiredCerts = $certs | Where-Object { 
                try {
                    [DateTime]::Parse($_.NotAfter) -lt (Get-Date)
                } catch {
                    $false
                }
            }
            $expiringCerts = $certs | Where-Object { 
                try {
                    $notAfter = [DateTime]::Parse($_.NotAfter)
                    $notAfter -gt (Get-Date) -and $notAfter -lt (Get-Date).AddDays(30)
                } catch {
                    $false
                }
            }
            
            if ($expiredCerts) {
                $problemsDetected.Add("Found $($expiredCerts.Count) expired certificate(s)") | Out-Null
            }
            if ($expiringCerts) {
                $problemsDetected.Add("Found $($expiringCerts.Count) certificate(s) expiring within 30 days") | Out-Null
            }
        }
    } else {
        $problemsDetected.Add("No Azure AD authentication certificates found") | Out-Null
    }

    # Check connectivity if file exists
    if (Test-Path "$outputPath\connectivity_test.csv") {
        $connTests = Import-Csv "$outputPath\connectivity_test.csv" -ErrorAction SilentlyContinue
        if ($connTests) {
            $failedTests = $connTests | Where-Object { -not $_.TcpTestSucceeded }
            if ($failedTests) {
                foreach ($test in $failedTests) {
                    $problemsDetected.Add("Connection failed to $($test.Endpoint)") | Out-Null
                }
            }
        }
    }

    # Check for authentication errors in event logs
    $aadEventFiles = Get-ChildItem -Path $outputPath -Filter "*AAD*events.csv" -ErrorAction SilentlyContinue
    if ($aadEventFiles) {
        $aadEvents = $aadEventFiles | ForEach-Object {
            Import-Csv $_.FullName -ErrorAction SilentlyContinue
        }
        $recentErrors = $aadEvents | Where-Object { 
            try {
                $_.LevelDisplayName -eq "Error" -and 
                [DateTime]::Parse($_.TimeCreated) -gt (Get-Date).AddHours(-24)
            } catch {
                $false
            }
        }
        if ($recentErrors) {
            $problemsDetected.Add("Found $($recentErrors.Count) authentication errors in the last 24 hours") | Out-Null
        }
    }

    $summary = @"
Authentication Diagnostics Summary
--------------------------------
Generated: $(Get-Date)
Username: $env:USERNAME
Computer: $env:COMPUTERNAME

PROBLEMS DETECTED:
$($problemsDetected.Count) issue(s) found
$(if ($problemsDetected.Count -gt 0) {
    $problemsDetected | ForEach-Object { "- $_`n" }
} else {
    "No significant problems detected"
})

OUTPUT FILES:
The following files have been generated (if issues were found):

- dsregcmd_status.txt: Device registration status
- dsregcmd_debug.txt: Detailed device registration debug info
- *_events.csv: Various event log exports
- connectivity_test.csv: Network connectivity results
- auth_certificates.csv: Authentication certificates
- browser_processes.csv: Browser process information

Please provide these files to your IT support team for analysis.
"@

    $summary | Out-File "$outputPath\summary.txt"
}

This will then export some diagnostic logs that you can manually go though to try to identify the problem as you can see below:


In the summary.txt you will find any issues that have been found as you can see below:

Authentication Diagnostics Summary
--------------------------------
Generated: 01/15/2025 15:43:47
Username: Lee.Croucher
Computer: BearWrk1

PROBLEMS DETECTED:
2 issue(s) found
- Device is not Azure AD joined
 - Primary Refresh Token is not present

Then if you enter the correct password the script will attempt to fix these issues where it can as you can see below: 


Previous Post Next Post

نموذج الاتصال