Private/Update-AccessTokenFromRefreshToken.ps1

function Update-AccessTokenFromRefreshToken {
    <#
    .SYNOPSIS
        Silently refreshes an access token using a refresh token.
 
    .DESCRIPTION
        Silently refreshes an access token using a refresh token obtained from a previous authentication.
        This function allows for unattended token renewal without requiring user interaction.
 
    .PARAMETER TenantID
        Tenant ID of the Entra ID tenant.
 
    .PARAMETER ClientID
        Application ID (Client ID) for an Entra ID service principal.
 
    .PARAMETER RefreshToken
        The refresh token obtained from a previous authentication response.
 
    .PARAMETER Scopes
        Array of permission scopes to request. Defaults to the original scopes used during authentication.
 
    .NOTES
        Author: Nickolaj Andersen
        Contact: @NickolajA
        Created: 2026-01-04
        Updated: 2026-01-18
 
        Version history:
        1.0.0 - (2026-01-04) Function created
        1.0.1 - (2026-01-18) Fixed Issue #208: Added refresh token storage and offline_access scope to ensure subsequent token refreshes work properly
    #>

    param(
        [parameter(Mandatory = $true, HelpMessage = "Tenant ID of the Entra ID tenant.")]
        [ValidateNotNullOrEmpty()]
        [String]$TenantID,

        [parameter(Mandatory = $true, HelpMessage = "Application ID (Client ID) for an Entra ID service principal.")]
        [ValidateNotNullOrEmpty()]
        [String]$ClientID,

        [parameter(Mandatory = $true, HelpMessage = "The refresh token obtained from a previous authentication response.")]
        [ValidateNotNullOrEmpty()]
        [String]$RefreshToken,

        [parameter(Mandatory = $true, HelpMessage = "Array of permission scopes to request.")]
        [ValidateNotNullOrEmpty()]
        [String[]]$Scopes
    )
    Process {
        try {
            Write-Verbose -Message "Attempting to refresh access token using refresh token"

            # Build token refresh request
            $TokenUri = "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/token"
            $ScopeString = $Scopes -join " "
            
            $TokenBody = @{
                "client_id" = $ClientID
                "scope" = $ScopeString
                "refresh_token" = $RefreshToken
                "grant_type" = "refresh_token"
            }

            # Request new access token using refresh token
            $TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $TokenBody -ErrorAction Stop

            # Validate the result
            if (-not $TokenResponse.access_token) {
                throw "No access token was returned in the response"
            }

            Write-Verbose -Message "Successfully refreshed access token"

            # Add ExpiresOn property for token expiration tracking
            $TokenResponse | Add-Member -MemberType NoteProperty -Name "ExpiresOn" -Value ((Get-Date).AddSeconds($TokenResponse.expires_in).ToUniversalTime()) -Force
            
            # Add Scopes property for permission tracking
            $TokenResponse | Add-Member -MemberType NoteProperty -Name "Scopes" -Value ($TokenResponse.scope -split " ") -Force
            
            # Add AccessToken property for consistent access
            $TokenResponse | Add-Member -MemberType NoteProperty -Name "AccessToken" -Value $TokenResponse.access_token -Force
            
            # Store refresh token if available for subsequent silent token renewals
            if ($TokenResponse.refresh_token) {
                $TokenResponse | Add-Member -MemberType NoteProperty -Name "RefreshToken" -Value $TokenResponse.refresh_token -Force
                Write-Verbose -Message "Refresh token stored for subsequent silent token renewals"
            }
            else {
                Write-Warning -Message "No refresh token returned in refresh response. Token refresh may not work in future requests."
            }

            # Update global variable
            $Global:AccessToken = $TokenResponse
        }
        catch {
            throw "Error refreshing access token: $($_)"
        }
    }
}