functions/core/Update-MgaAccessToken.ps1

function Update-MgaAccessToken {
    <#
    .SYNOPSIS
        Updates an existing access token
 
    .DESCRIPTION
        Updates an existing access token for contacting the specified application endpoint as long
        as the token is still valid. Otherwise, a new access is called through New-MgaAccessToken.
 
    .PARAMETER Token
        The token object to renew.
 
    .PARAMETER Register
        Registers the renewed token, so all subsequent calls to Exchange Online reuse it by default.
 
    .PARAMETER PassThru
        Outputs the token to the console, even when the register switch is set
 
    .EXAMPLE
        PS C:\> Update-MgaAccessToken -Register
 
        Updates the default (registered) Accesstoken and register it again as the default token
 
    .EXAMPLE
        PS C:\> $token = Update-MgaAccessToken -Token $token
 
        Updates the AccessToken in $token and output a new AccessToken object.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(DefaultParameterSetName = "Default")]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [MSGraph.Core.AzureAccessToken]
        $Token,

        [Parameter(ParameterSetName = 'Register')]
        [switch]
        $Register,

        [Parameter(ParameterSetName = 'Register')]
        [switch]
        $PassThru
    )

    begin {
        $endpointBaseUri = (Get-PSFConfigValue -FullName MSGraph.Tenant.Authentiation.Endpoint -Fallback 'https://login.microsoftonline.com')
        $baselineTimestamp = [datetime]"1970-01-01Z00:00:00"
    }

    process {
        $Token = Resolve-Token -Token $Token -FunctionName $MyInvocation.MyCommand

        $Credential = $Token.Credential
        $ClientId = $Token.ClientId
        $RedirectUrl = $Token.AppRedirectUrl.ToString()
        $ResourceUri = $Token.Resource.ToString().TrimEnd('/')
        $Permission = ($Token.Scope | Where-Object { $_ -notin "offline_access", "openid", "profile", "email" })
        $IdentityPlatformVersion = $Token.IdentityPlatformVersion

        if (-not $Token.IsValid) {
            Write-PSFMessage -Level Warning -Message "Token lifetime already expired and can't be newed. New authentication is required. Calling New-MgaAccessToken..." -Tag "Authorization"

            $paramsNewToken = @{
                PassThru                = $True
                ClientId                = $ClientId
                RedirectUrl             = $RedirectUrl
                ResourceUri             = $ResourceUri
                Permission              = $Permission
                IdentityPlatformVersion = $IdentityPlatformVersion
            }
            if ($Credential) { $paramsNewToken.Add("Credential", $Credential ) }
            if ($Register -or ($script:msgraph_Token.AccessTokenInfo.Payload -eq $Token.AccessTokenInfo.Payload) ) { $paramsNewToken.Add("Register", $true) }
            if (Test-PSFParameterBinding -ParameterName Verbose) { $paramsNewToken.Add("Verbose", $true) }

            $resultObject = New-MgaAccessToken @paramsNewToken
            if ($PassThru) { return $resultObject } else { return }
        }

        Write-PSFMessage -Level Verbose -Message "Start token refresh for application $( if($Token.AppName){$Token.AppName}else{$ClientId} ). (Identity platform version $($IdentityPlatformVersion))" -Tag "Authorization"

        switch ($IdentityPlatformVersion) {
            '1.0' { $endpointUriToken = "$($endpointBaseUri)/common/oauth2/token" }
            '2.0' {
                if ($token.Credential) {
                    $endpointUriToken = "$($endpointBaseUri)/organizations/oauth2/V2.0/token"
                } else {
                    $endpointUriToken = "$($endpointBaseUri)/common/oauth2/V2.0/token"
                }

                [array]$scopes = "offline_access", "openid"
                foreach ($permissionItem in $Permission) {
                    $scopes = $scopes + "$($resourceUri)/$($permissionItem)"
                }
                $scope = [string]::Join(" ", $scopes)
                Remove-Variable -Name scopes -Force -WhatIf:$false -Confirm:$false -Verbose:$false -Debug:$false
                Write-PSFMessage -Level VeryVerbose -Message "Using scope: $($scope)" -Tag "Authorization"
            }
        }

        $queryHash = [ordered]@{
            grant_type    = "refresh_token"
            client_id     = $ClientId
            refresh_token = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token.RefreshToken))
        }
        switch ($IdentityPlatformVersion) {
            '1.0' { $queryHash.Add("resource", [System.Web.HttpUtility]::UrlEncode($resourceUri)) }
            '2.0' {
                $queryHash.Add("scope", [uri]::EscapeDataString($scope))
                $queryHash.Add("redirect_uri", [System.Web.HttpUtility]::UrlEncode($redirectUrl))
            }
        }
        $authorizationPostRequest = Convert-UriQueryFromHash $queryHash -NoQuestionmark

        $content = New-Object System.Net.Http.StringContent($authorizationPostRequest, [System.Text.Encoding]::UTF8, "application/x-www-form-urlencoded")
        $httpClient = New-HttpClient
        $clientResult = $httpClient.PostAsync([Uri]$endpointUriToken, $content)
        $jsonResponse = ConvertFrom-Json -InputObject $clientResult.Result.Content.ReadAsStringAsync().Result -ErrorAction Ignore
        if ($clientResult.Result.StatusCode -eq [System.Net.HttpStatusCode]"OK") {
            Write-PSFMessage -Level Verbose -Message "AccessToken renewal successful. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase)" -Tag "Authorization"
        } else {
            $httpClient.CancelPendingRequests()
            $msg = "Request for AccessToken failed. $($clientResult.Result.StatusCode.value__) ($($clientResult.Result.StatusCode)) $($clientResult.Result.ReasonPhrase) `n$($jsonResponse.error_description)"
            Stop-PSFFunction -Message $msg -Tag "Authorization" -EnableException $true -Exception ([System.Management.Automation.RuntimeException]::new($msg))
        }

        # Build output object
        $resultObject = New-Object -TypeName MSGraph.Core.AzureAccessToken -Property @{
            IdentityPlatformVersion = $IdentityPlatformVersion
            TokenType               = $jsonResponse.token_type
            AccessToken             = $null
            RefreshToken            = $null
            IDToken                 = $null
            Credential              = $Credential
            ClientId                = $ClientId
            Resource                = $resourceUri
            AppRedirectUrl          = $RedirectUrl
        }
        switch ($IdentityPlatformVersion) {
            '1.0' {
                $resultObject.Scope = $jsonResponse.scope -split " "
                $resultObject.ValidUntilUtc = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToUniversalTime()
                $resultObject.ValidFromUtc = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToUniversalTime()
                $resultObject.ValidUntil = $baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToLocalTime().AddHours( [int]$baselineTimestamp.AddSeconds($jsonResponse.expires_on).ToLocalTime().IsDaylightSavingTime() )
                $resultObject.ValidFrom = $baselineTimestamp.AddSeconds($jsonResponse.not_before).ToLocalTime().AddHours( [int]$baselineTimestamp.AddSeconds($jsonResponse.not_before).ToLocalTime().IsDaylightSavingTime() )
            }
            '2.0' {
                $resultObject.Scope = $jsonResponse.scope.Replace("$ResourceUri/", '') -split " "
                $resultObject.ValidUntilUtc = (Get-Date).AddSeconds($jsonResponse.expires_in).ToUniversalTime()
                $resultObject.ValidFromUtc = (Get-Date).ToUniversalTime()
                $resultObject.ValidUntil = (Get-Date).AddSeconds($jsonResponse.expires_in).ToLocalTime()
                $resultObject.ValidFrom = (Get-Date).ToLocalTime()
            }
        }

        # Insert token data into output object. done as secure string to prevent text output of tokens
        if ($jsonResponse.psobject.Properties.name -contains "refresh_token") { $resultObject.RefreshToken = ($jsonResponse.refresh_token | ConvertTo-SecureString -AsPlainText -Force) }
        if ($jsonResponse.psobject.Properties.name -contains "id_token") {
            $resultObject.IDToken = ($jsonResponse.id_token | ConvertTo-SecureString -AsPlainText -Force)
            $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.id_token
        }
        if ($jsonResponse.psobject.Properties.name -contains "access_token") {
            $resultObject.AccessToken = ($jsonResponse.access_token | ConvertTo-SecureString -AsPlainText -Force)
            if ($jsonResponse.access_token.Contains(".") -and $jsonResponse.access_token.StartsWith("eyJ")) {
                $resultObject.AccessTokenInfo = ConvertFrom-JWTtoken -Token $jsonResponse.access_token
            }
        }

        # Getting validity period out of AccessToken information
        if ($resultObject.AccessTokenInfo -and $resultObject.AccessTokenInfo.TenantID.ToString() -notlike "9188040d-6c67-4c5b-b112-36a304b66dad") {
            $resultObject.ValidUntilUtc = $resultObject.AccessTokenInfo.ExpirationTime.ToUniversalTime()
            $resultObject.ValidFromUtc = $resultObject.AccessTokenInfo.NotBefore.ToUniversalTime()
            $resultObject.ValidUntil = $resultObject.AccessTokenInfo.ExpirationTime.ToLocalTime().AddHours( [int]$resultObject.AccessTokenInfo.ExpirationTime.ToLocalTime().IsDaylightSavingTime() )
            $resultObject.ValidFrom = $resultObject.AccessTokenInfo.NotBefore.ToLocalTime().AddHours( [int]$resultObject.AccessTokenInfo.NotBefore.ToLocalTime().IsDaylightSavingTime() )
        }

        # Checking if token is valid
        # ToDo implement "validating token information" -> https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-tokens
        if ($resultObject.IsValid) {
            if ($Register) {
                $script:msgraph_Token = $resultObject
                if ($PassThru) { $resultObject }
            } else {
                $resultObject
            }
        } else {
            Stop-PSFFunction -Message "Token failure. Acquired token is not valid" -EnableException $true -Tag "Authorization"
        }
    }

    end {
    }
}