Private/Refresh-JwtToken.ps1

<#
.SYNOPSIS
Refresh an expired JWT session token.

.DESCRIPTION
Exchanges the social-login session's refresh token for a fresh JWT access token via the Dune auth `/access-token` endpoint.
On success the in-memory `$DuneSession` access token and expiry date are updated and the session is re-cached.

Only `SocialLogin` sessions carry a refresh token. If the session is missing, is not a social login, or the refresh token
is rejected/expired, the session is cleared and the user is asked to reauthenticate with Connect-Dune.

.EXAMPLE
PS> Refresh-JwtToken
Refreshes the current social-login session's access token in place.
#>

function Refresh-JwtToken {
    [CmdletBinding()]
    param()

    if (-not $DuneSession) {
        throw "You are not authenticated. Please run Connect-Dune."
    }
    if ($DuneSession.Type -ne 'SocialLogin' -or -not $DuneSession.RefreshToken) {
        throw "Session expired and cannot be refreshed. Please run Connect-Dune to reauthenticate."
    }

    $RefreshToken = ConvertFrom-DuneSecureString -SecureString $DuneSession.RefreshToken

    $AuthUrl = "{0}{1}" -f $DuneSession.DuneApiUrl, "/access-token"
    $Headers = @{
        "Accept"       = "application/json"
        "Content-Type" = "application/json"
        "X-Tenant"     = $DuneSession.Tenant
    }

    $WebRequest = @{
        Uri             = $AuthUrl
        Method          = 'POST'
        Headers         = $Headers
        Body            = (@{ refreshToken = $RefreshToken } | ConvertTo-Json)
        UseBasicParsing = $true
    }
    if (([System.Uri]$DuneSession.DuneApiUrl).Host -eq 'localhost') { $WebRequest.SkipCertificateCheck = $true }

    Write-Debug "$($MyInvocation.MyCommand)|process|Refreshing access token ..."
    try {
        $Response = Invoke-WebRequest @WebRequest
    }
    catch {
        # Refresh token rejected/expired -> the session is no longer usable.
        if ($script:DuneSession) { Remove-Variable -Name DuneSession -Scope Script -Force }
        Remove-CachedDuneSession
        throw "Failed to refresh JWT token: $($_.Exception.Message). Please run Connect-Dune to reauthenticate."
    }

    # The auth service is configured with UseTokenCookie, so GetAccessToken returns an empty
    # body ({}) and delivers the new JWT in an 'ss-tok' Set-Cookie header. Fall back to the
    # response body for servers that return the token inline instead.
    $NewToken = $null
    try { $NewToken = ($Response.Content | ConvertFrom-Json).accessToken } catch { }
    if (-not $NewToken) {
        $SetCookie = $Response.Headers['Set-Cookie']
        if ($SetCookie -is [array]) { $SetCookie = $SetCookie -join '; ' }
        if ($SetCookie -match 'ss-tok=([^;]+)') { $NewToken = $Matches[1] }
    }
    if (-not $NewToken) {
        throw "Token refresh did not return an access token."
    }

    $ParsedToken = Parse-JwtToken -Token $NewToken
    # Use local time: the expiry is compared against Get-Date (local) in Assert-DuneSession.
    $TokenExpiryDate = [System.DateTimeOffset]::FromUnixTimeSeconds($ParsedToken.exp).LocalDateTime

    $Script:DuneSession.Token = ($NewToken | ConvertTo-SecureString -AsPlainText -Force)
    $Script:DuneSession.ExpiryDate = $TokenExpiryDate

    Write-Verbose "JWT token refreshed successfully"
    Save-DuneSession
}