wks_graphapi.psm1

<#
For long running workflow, manage internally the token query / renew
#>

$global:tokens = $null
$global:tokensDate = $null


<#############################################
 
FUNCTIONS
 
##############################################>


<#
    get-graphToken
    Retrieves application token for use with Graph API
#>


function get-graphToken {
    [CmdletBinding()]
    param(
        $config,
        $appSecret
    )

    $Url = "https://login.microsoftonline.com/$($config.TenantName)/oauth2/v2.0/token"

    # Add System.Web for urlencode
    Add-Type -AssemblyName System.Web

    # Create body
    $Body = @{
        client_id = $config.AppId
        client_secret = $appSecret
        scope = $config.Scope
        grant_type = 'client_credentials'
    }

    # Splat the parameters for Invoke-Restmethod for cleaner code
    $PostSplat = @{
        ContentType = 'application/x-www-form-urlencoded'
        Method = 'POST'
        # Create string by joining bodylist with '&'
        Body = $Body
        Uri = $Url
    }
    Write-Verbose "Query Token"
    # Request the token!
    Invoke-RestMethod @PostSplat
    
}

<#
    use-graphApi
    Generic function for calling Graph API.
    Includes token querying and error handling
#>


function use-graphApi {
    [cmdletbinding()]
    param(
        [string]$uri,
        [string]$method = 'get',
        [switch]$all,
        [switch]$beta,
        [switch]$raw,
        [string]$contentType='application/json; charset=utf-8',
        $body = $null,
        $token
    )
    $retryCount = 1
    $graphUrl = "https://graph.microsoft.com/"
    $version = 'v1.0/'
    if ($beta) {$version = 'beta/'}

    $uri = $graphUrl + $version + $uri
    # Create header
    $Header = @{
        Authorization = "$($token.token_type) $($token.access_token)"
    }

    write-debug ($header |ConvertTo-Json -Depth 5)
    
    do {
        try {
            $result = @() 
            if ($body -ne $null) {
                if ($contentType -like "*application/json*") { $body = $body |convertto-json -Depth 10 }
                write-verbose $Uri
                write-verbose $body
                $temp = Invoke-RestMethod -Uri $Uri -Headers $Header -Method $method -Body $body -ContentType $contentType
                if ($raw) { $result += $temp} 
                else { $result += $temp.value }
            } else {
                $temp = Invoke-RestMethod -Uri $Uri -Headers $Header -Method $method
                if ($raw) { $result += $temp} 
                else { $result += $temp.value }
                if ($all) {
                    while($temp."@odata.nextLink") {
                        $temp = Invoke-RestMethod -Uri ([uri]($temp."@odata.nextLink")) -Headers $Header -Method $method -ContentType $contentType
                        if ($raw) { $result += $temp} 
                        else {$result += $temp.value}
                    }
                }
                
            }
            $retryCount = 0
            $result
        } catch {
            if ($_.Exception.Response -eq $null) {
                Write-Error $_.Exception.Message
                
                #
                if ($retryCount -eq 0) {throw $_}
                else {Write-Warning "Retry #$retryCount"}
                $retryCount--
                
            }
            else {
                $Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
                $Reader.BaseStream.Position = 0
                $Reader.DiscardBufferedData()
                $ResponseBody = $Reader.ReadToEnd()
                if ($ResponseBody.StartsWith('{')) {
                    $ResponseBody = $ResponseBody | ConvertFrom-Json
                }

                #$ResponseBody.error
                $msg = $ResponseBody.error.code + " => " + $ResponseBody.error.message
                if ($ResponseBody.error.code -like 'InvalidAuthenticationToken') {
                    $msg = $msg + " Please run 'get-graphToken' first."
                }
                throw $msg
            }
        }
    } while ($retryCount -gt 0)
    
}

function use-graphApiBatchGet {
    [cmdletbinding()]
    param(
        [string[]]$uris,
        [switch]$beta,
        $token
    )

    
    $graphUrl = "https://graph.microsoft.com/"
    $version = 'v1.0/'
    if ($beta) {$version = 'beta/'}

    $uri = $graphUrl + $version + '$batch'
    # Create header
    $Header = @{
        Authorization = "$($token.token_type) $($token.access_token)"
        "Content-Type" = "application/json"
    }

    
    $thisHeader = @{
        "Content-Type" = "application/json"
    }
 
    $thisRequest = @()
    for($i = 0; $i -lt @($uris).count; $i++) {
        $thisRequest += @{
            "url" = $uris[$i]
            "method" = "GET"
            "id" = "$($i + 1)"
        }
    }

    try {
        $body = (@{"requests"= $thisRequest} |convertto-json -Depth 5)
        Write-Verbose $body
        $responses = (Invoke-RestMethod -Uri $Uri -Headers $Header -Method 'POST' -Body $body).responses
       
        $reparses = @($responses |% { @{next=$_.body."@odata.nextLink"; id=$_.id} |select -Unique |? {$_.next -ne $null}})
        Write-Verbose "Reparse $($reparses.count) URL: $($reparses -join ', ')"
        foreach ($reparse in $reparses) {
            write-verbose ($reparse |ConvertTo-Json)
            while($reparse.next) {
                $temp = Invoke-RestMethod -Uri ([uri]($reparse.next)) -Headers $Header -Method 'GET' -ContentType "application/json"
                write-verbose ($temp |ConvertTo-Json -depth 4)
                $data = $responses|? {$_.id -eq $reparse.id} |select -first 1
                if ($data) {
                    $data.body.value += $temp.value
                } else {
                    Write-Error "not found response with id $($reparse.id)"
                }
                $reparse = @{next=$temp."@odata.nextLink"; id=$reparse.id}
            }
        }
        $responses
    } catch {
        if ($_.Exception.Response -eq $null) {
            throw $_.Exception.Message
        }
        else {
            $Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
            $Reader.BaseStream.Position = 0
            $Reader.DiscardBufferedData()
            $ResponseBody = $Reader.ReadToEnd()
            if ($ResponseBody.StartsWith('{')) {
                $ResponseBody = $ResponseBody | ConvertFrom-Json
            }
            #$ResponseBody.error
            $msg = $ResponseBody.error.code + " => " + $ResponseBody.error.message
            if ($ResponseBody.error.code -like 'InvalidAuthenticationToken') {
                $msg = $msg + " Please run 'get-graphToken' first."
            }
            throw $msg
        }
    }

}


<#
    get-graphUsers
    Retrieve all tenant users
#>

function get-graphUsers {
    param(
        [string[]]$properties=$null,
        [string]$usertype='', # member or guest
        $token
    )
    $Uri = 'users'
    $params = @()
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $params += '$select=' + ($properties -join ',')
    }
    if ($usertype -ne '') {
        $params += '$filter=userType eq ''' + $userType + ''''
    }
    if ($params.count -gt 0) {
        $uri = $uri + '?' + ($params -join '&')
    }
    #write-host $uri
    use-graphApi -uri $uri -method 'get' -token $token -all
    
}

<#
    get-graphSharedItemPermissions
    Retrieve a simplified view of an item's share permissions
#>

function get-graphSharedItemPermissions {
    param(
        [string]$userId,
        [string]$itemId,
        [string[]]$internalDomains,
        $token
    )

    $Uri = "drives/$userId/items/$itemId/permissions"
    $result = @(use-graphApi -uri $uri -method 'get' -token $token -all)
    $response = @()
    foreach ($perm in $result) {
        if ($null -ne $perm.grantedToidentities) {
            foreach ($entity in $perm.grantedToidentities) {
                $response += New-Object -TypeName PSObject -Property @{
                    role = $perm.roles -join ','
                    scope = $perm.link.scope
                    email = $entity.user.email
                    name = $entity.user.displayName
                    link = $perm.link.webUrl
                    external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false}
                }
            }
        } elseif ($null -ne $perm.grantedTo) {
            foreach ($entity in $perm.grantedTo) {
                $response += New-Object -TypeName PSObject -Property @{
                    role = $perm.roles -join ','
                    scope = 'direct'
                    email = $entity.user.email
                    name = $entity.user.displayName
                    link = ''
                    external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false}
                }
            }
        } else {
            $response += New-Object -TypeName PSObject -Property @{
                role = $perm.roles -join ','
                scope = $perm.link.scope
                link = $perm.link.webUrl
            }
        }
        
    }

    #$result
    
    $response
}

function get-graphSharedItemPermissionsBatch {
    [CmdletBinding()]
    param(
        [string]$userId,
        [string[]]$itemIds,
        [string[]]$internalDomains,
        $token,
        $maxRetryCount = 5,
        [int]$sleep = 1
    )
    $retryList = $itemIds
    $responses=@{ throttlingDuration = 0; errors = @(); data = @{}; processingDuration = 0}
    $toProcess = @()
    $throttlingDelay = 0
    $loopCount = 0
    $processingStart = get-date
    do {
        
        $Uris = @($retryList |% { "drives/$userId/items/$_/permissions"})
        Write-verbose "get-graphSharedItemPermissionsBatch: $($retryList -join ', ')"
        #write-verbose ($result |ConvertTo-Json -depth 5)
        $result = @(use-graphApiBatchGet -uri $uris -token $token -beta)
        $toProcess += $result |? { $_.status -eq 200 }
        # Comppute throttlong delay as max of all requested delay
        $throttlingDelay = 0
        $throttlingDelay = ($result |? { $_.status -eq 429 } |% { $_.headers."Retry-After" } |measure -Maximum).maximum
        # Show errors
        $result |? { ($_.status -ge 400) -and ($_.status -ne 429) } |% { 
            Write-error "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])"
            $responses.errors += "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])"
        }
        $retryList = @($result |? { $_.status -eq 429 } |% { $retryList[$_.id] }) |? { ($null -ne $_) -and ('' -ne $_)}
        if ($retryList.count -gt 0) {
            Write-Warning "Azure Graph API applied $throttlingDelay sec. throttling to $($retryList -join ', ')"
            $responses.throttlingDuration += new-timespan -seconds $throttlingDelay
        }
        if ($throttlingDelay -gt 0) { Start-Sleep -Seconds $throttlingDelay }
        else { Start-Sleep -Seconds $sleep }
        $loopCount++
    } while (($retryList.count -gt 0) -and ($loopCount -lt $maxRetryCount))
    if ($loopCount -eq $maxRetryCount) { Write-Warning "get-graphSharedItemPermissionsBatch maxRetryCount ($maxRetryCount) reached with $($retryList.count) items ignored" }
    
    foreach ($data in $toProcess) {
        #write-debug ($data |convertto-json -Depth 10)
        $itemId = $itemIds[($data.id - 1)]
        $responses.data[$itemId] = @()
        
        # Build list of permIds to prevent duplicates
        $permIds = @()
        Write-verbose "- Item $itemId -> Permissions count: $($data.body.value.count)"
        foreach ($perm in $data.body.value) {
            # New permission to process for this item
            if ($permIds -notcontains $perm.id) {
                $permIds += $perm.id
                if ($null -ne $perm.grantedToidentities) {
                    foreach ($entity in $perm.grantedToidentities) {
                        $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{
                            role = $perm.roles -join ','
                            scope = if ($perm.link.scope) {$perm.link.scope} else {''}
                            id = $perm.id
                            email = $entity.user.email
                            name = $entity.user.displayName
                            link = $perm.link.webUrl
                            inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false}
                            external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false}
                        }
                        
                    }
                } elseif ($null -ne $perm.grantedTo) {
                    foreach ($entity in $perm.grantedTo) {
                        $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{
                            id = $perm.id
                            role = $perm.roles -join ','
                            scope = 'direct'
                            email = $entity.user.email
                            name = $entity.user.displayName
                            link = ''
                            inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false}
                            external = if ($entity.user.email) { if ($internalDomains -notcontains ($entity.user.email -split '@')[1]) {$true} else { $false} } else {$false}
                        }
                    }
                } else {
                    $responses.data[$itemId] += New-Object -TypeName PSObject -Property @{
                        id = $perm.id
                        role = $perm.roles -join ','
                        scope = if ($perm.link.scope) {$perm.link.scope} else {''}
                        link = $perm.link.webUrl
                        inherited = if ($null -ne $perm.inheritedFrom) {$true} else {$false}
                    }
                }
            }
        }
        
        if ($responses.data[$itemId].count -eq 0) {
            Write-Debug "Failed retrieving permission for item $itemId : $($data.body.error.code) -> $($data.body.error.message)"
        }
    }

    $responses.processingDuration = (get-date) - $processingStart

    $responses

    
}


<#
    get-graphSharedFiles
    Recursively parse an users's for shared files and returns a search result
#>

function get-graphSharedFiles {
    [CmdletBinding(DefaultParameterSetName = 'token')]
    param (
        [Parameter(Mandatory = $true,ParameterSetName = 'token')]
        [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string]$itemId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'token',
            HelpMessage = 'Graph API token object')]$token,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'Graph API configuration')]$config,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'App Registration Secret')]$secret
    )
    
    $internalToken = $token
    # Get / Refresh Token if parameterSet Autonomous (for long run)
    if (($null -ne $config) -and ($null -ne $secret)) {
        if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) {
            Write-Verbose "Querying autonomous token for get-graphSharedFiles"
            $internalToken = get-graphToken -config $config -appSecret $secret
            $global:tokens = $internalToken
            $global:tokensDate = get-date
        } else {
            $internalToken = $global:tokens
        }
    }
    
    $item = "root"
    $sharedResult = New-Object -TypeName PSObject -Property @{
        userid=$userId
        sharedItemsCount=0
        totalItems=0
        startedAt=(get-date)
        endedAt=$null
        duration=$null
        sharedItems=@()
    }
    if ('' -ne $itemId) { $item = "items/$itemId"}
    
    $Uri = "drives/$userId/$item/children" + '?$top=10000&$inlinecount=allpages&$select=id,name,path,file,folder,shared,parentreference,weburl' 
    Write-Verbose $uri
    
    $result = use-graphApi -uri $uri -method 'get' -token $internalToken -all
    foreach ($i in $result) {
        $sharedResult.totalItems++
        $itemType = 'file'
        if ($null -ne $i.folder) { $itemType='folder'}
        #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))"
        if ($i.shared.scope.count -gt 0) {
            # The item is shared. Build metadata
            $sharedResult.sharedItemsCount++
            $newItem = New-Object -TypeName PSObject -Property @{
                userid=$userId
                itemid=$i.id
                name=$i.name
                scope=$i.shared.scope
                path=($i.parentreference.path -split '/root:')[1]
                type=$itemType
                sharedWith = get-graphSharedItemPermissions -userId $userId -itemId $i.id -internalDomains $internalDomains -token $internalToken
            }
            #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) }
            #Write-Host "-> SHARED"
            $sharedResult.sharedItems += $newItem
        } else {
            # Parse only non-shared sub-folders
            
        }
        # Parse all sub-folders
        if ($null -ne $i.folder) {
            Write-Verbose "Parsing subfolder : $($i.name)"
            $temp = @(get-graphSharedFiles -userid $userId -itemid $i.id -internalDomains $internalDomains -token $internalToken)
            if ($temp.sharedItemsCount -gt 0) {
                $sharedResult.sharedItems += $temp.sharedItems
                $sharedResult.totalItems += $temp.totalItems
                $sharedResult.sharedItemsCount += $temp.sharedItemsCount
            }
            $temp = $null
        }
    }
    
    $sharedResult.endedAt=(get-date)
    $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt)
    $sharedResult
}

function get-graphReportOneDriveAccountDetail {
    [CmdletBinding()]
    param(
        $token
    )

    $Uri = 'reports/getOneDriveUsageAccountDetail(period=''D7'')?$format=application/json'
    use-graphApi -uri $uri -method 'get' -token $token -all -beta
}

function get-graphSharedFilesBatch {
    [CmdletBinding(DefaultParameterSetName = 'token')]
    param (
        [Parameter(Mandatory = $true,ParameterSetName = 'token')]
        [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string[]]$itemId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'token',
            HelpMessage = 'Graph API token object')]$token,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'Graph API configuration')]$config,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'App Registration Secret')]$secret,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][switch]$beta
    )
    
    $internalToken = $token
    # Get / Refresh Token if parameterSet Autonomous (for long run)
    if (($null -ne $config) -and ($null -ne $secret)) {
        if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) {
            Write-Verbose "Querying autonomous token for get-graphSharedFiles"
            $internalToken = get-graphToken -config $config -appSecret $secret
            $global:tokens = $internalToken
            $global:tokensDate = get-date
        } else {
            $internalToken = $global:tokens
        }
    }
    
    #$item = "root"
    $sharedResult = New-Object -TypeName PSObject -Property @{
        userid=$userId
        sharedItemsCount=0
        totalItems=0
        startedAt=(get-date)
        endedAt=$null
        duration=$null
        sharedItems=@()
    }
    
    $uri =@()
    if ($itemId.count -ne 0) { $Uri = $itemId |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}}
    else {$Uri += ("drives/$userId/root/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl')}
    Write-Verbose ($uri -join ', ')
    
    $resultSet = use-graphApiBatchGet -uri $uri -token $internalToken -beta:$beta
    #write-host "- Processing $(@($resultSet).count) results"

    $subfolderIds = @()
    $result = @($resultset |% {$_.body.value})
    #write-host "- Processing $($result.count) items"
    # Process files
    foreach ($i in @($result |? {$_.file -ne $null})) {
        $sharedResult.totalItems++
        $itemType = 'file'
        # prepare object
        $newItem = New-Object -TypeName PSObject -Property @{
            userid=$userId
            itemid=$i.id
            name=$i.name
            scope=$i.shared.scope
            path=($i.parentreference.path -split '/root:')[1]
            type=$itemType
            sharedWith = ''
        }
        #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))"
        if ($i.shared.scope.count -gt 0) {
            # The item is shared. Build metadata
            $sharedResult.sharedItemsCount++
            
            #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) }
            #Write-Host "-> SHARED"
            $sharedResult.sharedItems += $newItem
        }
    }

    # process folders in batch mode
    foreach ($i in @($result |? {$_.folder -ne $null})) {
        
        $sharedResult.totalItems++
        $itemType = 'folder'
        $subfolderIds += $i.id
        # prepare object
        $newItem = New-Object -TypeName PSObject -Property @{
            userid=$userId
            itemid=$i.id
            name=$i.name
            scope=$i.shared.scope
            path=($i.parentreference.path -split '/root:')[1]
            type=$itemType
            sharedWith = ''
        }

        #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))"
        if ($i.shared.scope.count -gt 0) {
            # The item is shared. Build metadata
            $sharedResult.sharedItemsCount++
            
            #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) }
            #Write-Host "-> SHARED"
            $sharedResult.sharedItems += $newItem
        }
    }

    # Parse all parsed subfolders at once in batch mode
    if ($subfolderIds.count -gt 0) {
        # Batch process subfolderparsing
        $parallelismLevel = 20
        $iterations = [math]::Floor($subfolderIds.count / $parallelismLevel)
        Write-verbose "- $($subfolderIds.count) subfolder to parse"
        Write-verbose "Folder iterations: $iterations"
        for ($iteration=0; $iteration -le $iterations; $iteration++) {
            $count = $parallelismLevel
            if ($iteration -eq $iterations) {
                $count = $subfolderIds.count % $parallelismLevel
            }
            Write-verbose "Folder iteration loop: $iteration"
            Write-verbose "Folder iteration count: $count"
            $items = $subfolderIds |select -skip ($iteration * $parallelismLevel) -first $count
            #Write-Host "Parsing folder for $($items.count) items: $($items -join ', ')"
            $temp = get-graphSharedFilesBatch -userid $userId -itemid $items -internalDomains $internalDomains -token $internalToken -beta:$beta
            
            #write-verbose ($responses |convertto-json -Depth 5)
            #foreach ($i in $temp) {
                $sharedResult.totalItems += $temp.totalItems
                if ($temp.sharedItemsCount -gt 0) {
                    $sharedResult.sharedItems += $temp.sharedItems
                    $sharedResult.sharedItemsCount += $temp.sharedItemsCount
                }
            #}
            
            $temp = $null
        }
    }


    $sharedResult.endedAt=(get-date)
    $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt)
    $sharedResult
}

# Non-recursive version
function get-graphSharedFilesBatch2 {
    [CmdletBinding(DefaultParameterSetName = 'token')]
    param (
        [Parameter(Mandatory = $true,ParameterSetName = 'token')]
        [Parameter(Mandatory = $true,ParameterSetName = 'autonomous')][string]$userId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string[]]$itemId,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][string[]]$internalDomains,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'token',
            HelpMessage = 'Graph API token object')]$token,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'Graph API configuration')]$config,
        [Parameter(Mandatory = $true,
            ParameterSetName = 'autonomous',
            HelpMessage = 'App Registration Secret')]$secret,
        [Parameter(ParameterSetName = 'token')]
        [Parameter(ParameterSetName = 'autonomous')][switch]$beta
    )
    
    $internalToken = $token
    # Get / Refresh Token if parameterSet Autonomous (for long run)
    if (($null -ne $config) -and ($null -ne $secret)) {
        if (($null -eq $global:tokensDate) -or ((($global:tokensDate - (get-date)).minutes -gt 10))) {
            Write-Verbose "Querying autonomous token for get-graphSharedFiles"
            $internalToken = get-graphToken -config $config -appSecret $secret
            $global:tokens = $internalToken
            $global:tokensDate = get-date
        } else {
            $internalToken = $global:tokens
        }
    }
    
    #$item = "root"
    $sharedResult = New-Object -TypeName PSObject -Property @{
        userid=$userId
        sharedItemsCount=0
        totalItems=0
        totalFolders=0
        startedAt=(get-date)
        endedAt=$null
        duration=$null
        sharedItems=@()
        errors=@()
        throttlingDuration = 0
    }
    # count current depth folder level
    $depth = 0

    # Build URI
    
    $subfolderIds = @()
    # Process a folder depth at once and go deeper
    do {
        Write-host "- Processing depth level $depth"
        $uri =@()
        # in first loop use function's input parameters if provided
        $result  = @()
        if ($depth -eq 0) {
            # Build URI list for Graph API batch processing
            if ($itemId.count -ne 0) { $Uri = $itemId |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}}
            # If no itemid provided start at root level
            else {$Uri += ("drives/$userId/root/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl')}
            Write-Verbose ($uri -join ', ')
            $resultset = use-graphApiBatchGet -uri $uri -token $internalToken -beta:$beta
            $result += @($resultset |% {$_.body.value})
            $errors = @($resultset |? { $_.status -ne 200 })
            $sharedResult.errors+=$errors
        } else {
            # Batch process subfolderparsing
            $parallelismLevel = 20
            $iterations = [math]::Floor($subfolderIds.count / $parallelismLevel)
            Write-verbose "- $($subfolderIds.count) subfolder to parse"
            Write-verbose "Folder iterations: $iterations"
            for ($iteration=0; $iteration -le $iterations; $iteration++) {
                $count = $parallelismLevel
                if ($iteration -eq $iterations) {
                    $count = $subfolderIds.count % $parallelismLevel
                }
                Write-verbose "- Iteration loop: $iteration / $iterations"
                Write-verbose "Folder iteration count: $count"
                $items = @($subfolderIds |select -skip ($iteration * $parallelismLevel) -first $count)
                if ($items.count -gt 0) {
                    # Use next loop iteration to parse subfolders
                    $Uri = $items |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'}
                
                    $retryList = $items
                    $throttlingDelay = 0
                    $loopCount = 0
                    do {
                        
                        $Uris = @($retryList |% {"drives/$userId/items/$_/children" + '?$top=10000&$select=id,name,path,file,folder,shared,parentreference,weburl'})
                        Write-verbose "get-graphSharedFilesBatch2: $($Uris -join ', ')"
                        #write-verbose ($result |ConvertTo-Json -depth 5)
                        $resultSet = @(use-graphApiBatchGet -uri $uris -token $internalToken -beta:$beta)
                        #Collect results to process
                        $result += @($resultSet |? { $_.status -eq 200 }  |% {$_.body.value})
                        # Comppute throttlong delay as max of all requested delay
                        $throttlingDelay = 0
                        $throttlingDelay = ($resultSet |? { $_.status -eq 429 } |% { $_.headers."Retry-After" } |measure -Maximum).maximum
                        # Show errors
                        $resultSet |? { ($_.status -ge 400) -and ($_.status -ne 429) } |% { 
                            $sharedResult.errors+= "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])"
                            Write-error "Error $($_.status) / $($_.body.error.message) on item $($retryList[$_.id])"
                        }
                        
                        # build retry list
                        $retryList = @($resultSet |? { $_.status -eq 429 } |% { $retryList[$_.id] })
                        if ($retryList.count -gt 0) {
                            Write-Warning "Azure Graph API applied $throttlingDelay sec. throttling to $($retryList -join ', ')"
                        }
                        if ($throttlingDelay -gt 0) { 
                            $sharedResult.throttlingDuration += $throttlingDelay
                            Start-Sleep -Seconds ($throttlingDelay) 

                        }
                        $loopCount++
                    } while (($retryList.count -gt 0) -and ($loopCount -lt $maxRetryCount))
                    if ($loopCount -eq $maxRetryCount) { 
                        Write-Warning "get-graphSharedItemPermissionsBatch maxRetryCount ($maxRetryCount) reached"
                        if ($retryList.count -gt 0) { Write-error "$($retryList.count) childitem items ignored: $($retryList -join ', ')" }
                    }
                    


                }
                # Wait 1 sec every 2 queries
                if (($iteration / 2) -eq 1) { Start-Sleep -seconds 1}
            }
            # clear list for further processing
            $subfolderIds = @()
            
        }
        
        # Process results

        write-verbose "- Processing $($result.count) items"
        
        # Process files
        foreach ($i in @($result |? {$_.file -ne $null})) {
            $sharedResult.totalItems++
            $itemType = 'file'
            # prepare object
            $newItem = New-Object -TypeName PSObject -Property @{
                userid=$userId
                itemid=$i.id
                name=$i.name
                scope=$i.shared.scope
                path=($i.parentreference.path -split '/root:')[1]
                parentid=$i.parentreference.id
                type=$itemType
                sharedWith = @()
            }
            #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))"
            if ($i.shared.scope.count -gt 0) {
                # The item is shared. Build metadata
                $sharedResult.sharedItemsCount++
                
                #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) }
                #Write-Host "-> SHARED"
                $sharedResult.sharedItems += $newItem
            }
        }

        # process folders in batch mode
        foreach ($j in @($result |? {$_.folder -ne $null})) {
            $sharedResult.totalItems++
            $sharedResult.totalFolders++
            $itemType = 'folder'
            $subfolderIds += $j.id
            # prepare object
            $newItem = New-Object -TypeName PSObject -Property @{
                userid=$userId
                itemid=$j.id
                name=$j.name
                scope=$j.shared.scope
                path=($j.parentreference.path -split '/root:')[1]
                parentid=$j.parentreference.id
                type=$itemType
                sharedWith = @()
            }

            #Write-Host "$(($i.path.parentreference -split '/root:')[1])/$($i.name) ($($itemtype))"
            if ($j.shared.scope.count -gt 0) {
                # The item is shared. Build metadata
                $sharedResult.sharedItemsCount++
                #if ($includePermissions) { $newItem.sharedWith = @(get-graphSharedItemPermissions -userId $userId -itemId $i.id) }
                #Write-Host "-> SHARED"
                $sharedResult.sharedItems += $newItem
            }
        }
        # increase depth level
        $depth++
        Write-verbose "- Finished processing depth level $depth"
        Write-host "- Need to process $($subfolderIds.count) subfolder at next depth level"
    } while ($subfolderIds.count -gt 0)

    $sharedResult.endedAt=(get-date)
    $sharedResult.duration=($sharedResult.endedAt - $sharedResult.startedAt)
    $sharedResult
}


function get-graphSharedFilesPermissionsBatch {
    [CmdletBinding()]
    param(
        $userId,
        $sharedItems,
        $internalDomains,
        $token
    )

    Write-host "Processing Permissions"
    # Retrive permissions of retrieved shared items
    if ($sharedItems.sharedItemsCount -gt 0) {
        $parallelismLevel = 20
        $iterations = [math]::Floor($sharedItems.sharedItemsCount / $parallelismLevel)
        Write-host "Permission total iterations: $iterations"
        for ($iteration=0; $iteration -le $iterations; $iteration++) {
            $count = $parallelismLevel
            if ($iteration -eq $iterations) {
                $count = $sharedItems.sharedItemsCount % $parallelismLevel
            }
            Write-Verbose "- Permission iteration loop: $iteration / $iterations"
            Write-Debug "Permission iteration count: $count"
            $items = $sharedItems.sharedItems |select -skip ($iteration * $parallelismLevel) -first $count
            Write-Debug "Requesting permissions for $($items.count) items: $(($items |select -ExpandProperty itemid) -join ', ')"
            $responses = get-graphSharedItemPermissionsBatch -userId $userId -itemIds ($items |select -ExpandProperty itemid) -internalDomains $internalDomains -token $token
            
            # Put permission back to item list
            #write-verbose ($responses |convertto-json -Depth 5)
            foreach ($i in $items) { 
                $i.sharedWith += $responses.data[$i.itemid]
                if ($i.sharedWith -eq 0) {
                    #write-error "Permissions empty for $($i.name) ($($i.itemid))"
                }
            }
            # Increment feedback (counters, timers, errors)
            $sharedItems.errors += $responses.errors
            $sharedItems.duration += $responses.processingDuration
            $sharedItems.throttlingDuration += $responses.throttlingDuration

            if (($iteration % 100) -eq 99) { 
                $delay = 30
                if ($iteration -gt 200) { $delay = 60}
                Write-warning "Preventive wait for $delay sec."
                Start-Sleep -seconds $delay
            }
        }
    }
    $sharedItems.endedAt=(get-date)       
    $sharedItems
}
<#
                 
     
                 
                #>


function send-graphMailReport {
    param(
        [Parameter(Mandatory=$true)][string]$senderId,
        [Parameter(Mandatory=$true)][string[]]$recipientAddress,
        [Parameter(Mandatory=$true)]$subject,
        [Parameter(Mandatory=$true)]$body,
        $htmlAttachment,
        [Parameter(Mandatory=$true)]$token
    )

    $message =  @{
        "message"= @{
            "subject"= "$subject"
            "body"= @{
                "contentType"= "html"
                "content"= "$body"
            }
            "toRecipients"= @()
        }
    }

    $recipientAddress |% { $message.message.toRecipients += @{
            "emailAddress" = @{
                "address" = "$_"
            }
        }
    }

    if (($htmlAttachment -ne '') -and ($null -ne $htmlAttachment)) {
        $message.message.Add('attachments', @(
            @{
                "@odata.type"= "#microsoft.graph.fileAttachment"
                "name"= "report.html"
                "contentType"= "text/html"
                "contentBytes"= "$([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($htmlAttachment)))"
            }
        ))
    }

    
    $uri ="users/$senderId/sendMail"
    
    #$message |convertto-json -depth 5
    use-graphApi -uri $uri -method "POST" -body $message -token $token -raw -contentType "application/json; charset=utf-8"
}

function get-graphDomains {
    param(
        $token
    )

    $Uri = 'domains'
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphGroups {
    param(
        [string[]]$properties=$null,
        $token
    )
    $Uri = 'groups'
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphApplications {
    param(
        [string[]]$properties=$null,
        $token
    )
    $Uri = 'applications'
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphApplication {
    param(
        [string]$id,
        [string[]]$properties=$null,
        $token
    )
    $Uri = "applications/$id"
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphServicePrincipals {
    param(
        [string]$id,
        [string[]]$properties=$null,
        $token
    )
    $Uri = "servicePrincipals"
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$top=999&$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphServicePrincipal {
    param(
        [string]$id,
        [string[]]$properties=$null,
        $token
    )
    $Uri = "servicePrincipals/$id"
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

function get-graphAadTermsAndConditions {
    param(
        [string]$id,
        [string[]]$properties=$null,
        $token
    )
    $Uri = "termsAndConditions"
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}


function get-graphDirectoryRoles {
    param(
        [string[]]$properties=$null,
        $token
    )
    $Uri = 'directoryRoles'
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}
function get-graphDirectoryRoleTemplates {
    param(
        [string[]]$properties=$null,
        $token
    )
    $Uri = 'directoryRoleTemplates'
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}

<#
    CONDITIONNAL ACCESS
#>

function get-graphCAPolicies {
    param(
        [string[]]$properties=$null,
        $token
    )
    $Uri = 'identity/conditionalAccess/policies'
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all -beta
}

function get-graphDirectoryObject {
    param(
        [string]$id,
        [string[]]$properties=$null,
        $token
    )
    $Uri = "directoryObjects/$id"
    if ($null -ne $properties) {
        $properties |% { $_ = $_.trim() }
        $uri += '?$select=' + ($properties -join ',')
    }
    use-graphApi -uri $uri -method 'get' -token $token -all
}


<#
    Access Token routines
#>


<#
    .SYNOPSIS
       Does a Multi-Factor authentication against Azure using UserName and Password and a One Time Password OTP as the second factor
  
    .DESCRIPTION
      Does a Multi-Factor authentication against Azure using UserName and Password and a One Time Password OTP as the second factor
  
    .INPUTS
       PSCrednetails which will contain the username and password for the Primary Auth
       OTP code can be generated with the Get-TimeBasedOneTimePassword function (requires that this be setup beforehand)
  
    .OUTPUTS
        AccessToken
  
    .EXAMPLE
        PS C:\> Get-AccessTokenMFA -OTP 123456
         
        Example use a SharedSecret stored in the Windows Credential Store
 
        PC C:\> Get-AccessTokenMFA -OTP (Get-TimeBasedOneTimePassword -SharedSecret (Get-StoredCredential -Target StoredAuth -AsCredentialObject).Password)
    .NOTES
        Author : Glen Scales
  
    .LINK
         
 
#>

function Get-AccessTokenMFA{
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [PSCredential]
        $Credential,
        [Parameter(Position = 1, Mandatory = $true)]
        [String]
        $OTP,
        [Parameter(Position = 2, Mandatory = $false)]
        [String]
        $ClientId = "95a1b05c-60f1-420d-a5b2-0cca170dfadc",
        [Parameter(Position = 3, Mandatory = $false)]
        [String]
        $RedirectURI = "https://login.microsoftonline.com/common/oauth2/nativeclient",
        [Parameter(Position = 4, Mandatory = $false)]
        [String]
        $scopes = "https://outlook.office.com/EWS.AccessAsUser.All"
    )
    process {        
        $domain = $Credential.UserName.Split('@')[1]
        $openidURL = "https://login.windows.net/$domain/v2.0/.well-known/openid-configuration"
        $TenantId = (Invoke-WebRequest -Uri $openidURL | ConvertFrom-Json).token_endpoint.Split('/')[3] 
        $AuthURL = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=$ClientId&response_mode=form_post&response_type=code&redirect_uri=" + [System.Web.HttpUtility]::UrlEncode($RedirectURI)
        $StartLogon = Invoke-WebRequest -uri $AuthURL  -SessionVariable 'AuthSession'
        $Context = [regex]::Match($StartLogon.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value
        $Flow = [regex]::Match($StartLogon.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value
        $Canary = [regex]::Match($StartLogon.RawContent,"`"canary`":`"(.*?)`"").Groups[1].Value
        $FBAAuthBody=@{
            "login" = $Credential.UserName
            "loginFmt" = $Credential.UserName
            "i13"="0"
            "type"="11"
            "LoginOptions"="3"
            "passwd"= $Credential.GetNetworkCredential().password.ToString()
            "ps"="2"
            "flowToken"=$Flow
            "canary"=$Canary
            "ctx"=$Context
            "NewUser"="1"
            "fspost"="0"
            "i21"="0"
            "CookieDisclosure"="1"
            "IsFidoSupported"="1"
            "hpgrequestid"=(New-Guid).ToString()
        }
        if (($Context -eq '') -or ($flow -eq '')) { throw "Authorize: $strServiceExceptionMessage. $($StartLogon.RawContent)" }
                
        $FBAResponse = Invoke-WebRequest -Uri "https://login.microsoftonline.com/common/login" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FBAAuthBody -WebSession $AuthSession
        $Context = [regex]::Match($FBAResponse.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value
        $Flow = [regex]::Match($FBAResponse.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value
        $strServiceExceptionMessage = [regex]::Match($FBAResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value
        $SasBegin=@{
            "AuthMethodId" = "PhoneAppOTP"
            "flowToken"=$Flow
            "ctx"=$Context
            "Method"="BeginAuth"
        }
        
        if (($Context -eq '') -or ($flow -eq '')) { throw "Login: $strServiceExceptionMessage. $($FBAResponse.RawContent)" }
        
        $SASBeginResponse = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/SAS/BeginAuth" -Method Post -ContentType "application/json" -Body ($SasBegin | ConvertTo-Json) -WebSession $AuthSession)
        $strServiceExceptionMessage = [regex]::Match($SASBeginResponse.RawContent,"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value
        if ($strServiceExceptionMessage -ne '') { throw "BeginAuth: $strServiceExceptionMessage" }
        $SASEnd=@{
            "AdditionalAuthData"=$OTP
            "AuthMethodId"="PhoneAppOTP"
            "flowToken"=$SASBeginResponse.flowToken
            "ctx"=$SASBeginResponse.ctx
            "Method"="EndAuth"
            "PollCount"=1
            "SessionId"=$SASBeginResponse.SessionId
        }

        if (($SASBeginResponse.flowToken -eq $null) -or ($SASBeginResponse.ctx -eq $null) -or ($SASBeginResponse.SessionId -eq '')) { throw "BeginAuth: $strServiceExceptionMessage. $($SASBeginResponse.RawContent)" }
        
        $SASEndResponse = (Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/SAS/EndAuth" -Method Post -ContentType "application/json" -Body ($SASEnd | ConvertTo-Json) -WebSession $AuthSession)
        $strServiceExceptionMessage = [regex]::Match($SASEndResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value
        if ($strServiceExceptionMessage -ne '') { throw "EndAuth: $strServiceExceptionMessage" }
        $SASProcess=@{
            "type"=19
            "GeneralVerify"="false"
            "otc"=$OTP
            "login"= $Credential.UserName
            "mfaAuthMethod"="PhoneAppOTP"
            "flowToken"=$SASEndResponse.flowToken
            "request"=$SASEndResponse.ctx
            "Method"="EndAuth"
            "PollCount"=1
            "SessionId"=$SASEndResponse.SessionId
            "canary"=$Canary
            "hpgrequestid"=(New-Guid).ToString()
        }

        if (($SASEndResponse.flowToken -eq '') -or ($SASEndResponse.ctx -eq '') -or ($SASEndResponse.SessionId -eq '')) { throw "EndAuth: $strServiceExceptionMessage. $($SASEndResponse.RawContent)" }

        $SASProcessResponse = (Invoke-WebRequest -Uri "https://login.microsoftonline.com/common/SAS/ProcessAuth" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $SASProcess -WebSession $AuthSession -ea SilentlyContinue)
        $strServiceExceptionMessage = [regex]::Match($SASProcessResponse.RawContent,"`"strServiceExceptionMessage`":`"(.*?)`"").Groups[1].Value
        if ($strServiceExceptionMessage -ne '') { throw "ProcessAuth: $strServiceExceptionMessage" }
        $formElements = ([XML]$SASProcessResponse.Content).GetElementsByTagName("input");  
        $authCode = ""
        foreach ($element in $formElements) {
            if ($element.Name -eq "code") {
                $authCode = $element.GetAttribute("value");
                Write-Verbose $authCode
            }
        }  
        

        $Body = @{"grant_type" = "authorization_code"; "scope" = $scopes; "client_id" = "$ClientId"; "code" = $authCode; "redirect_uri" = $RedirectURI }
        $tokenRequest = Invoke-RestMethod -Method Post -ContentType application/x-www-form-urlencoded -Uri "https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token" -Body $Body 
        return $tokenRequest
    }
}

function Get-AccessTokenNonMFA{
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [PSCredential]
        $Credential,
        [Parameter(Position = 1, Mandatory = $false)]
        [String]
        $ClientId = "95a1b05c-60f1-420d-a5b2-0cca170dfadc",
        [Parameter(Position = 2, Mandatory = $false)]
        [String]
        $RedirectURI = "https://login.microsoftonline.com/common/oauth2/nativeclient",
        [Parameter(Position = 3, Mandatory = $false)]
        [String]
        $scopes = "https://outlook.office.com/EWS.AccessAsUser.All"
    )
    process {        
        $domain = $Credential.UserName.Split('@')[1]
        $openidURL = "https://login.windows.net/$domain/v2.0/.well-known/openid-configuration"
        $TenantId = (Invoke-WebRequest -Uri $openidURL | ConvertFrom-Json).token_endpoint.Split('/')[3] 
        $AuthURL = "https://login.microsoftonline.com/common/oauth2/authorize?client_id=$ClientId&response_mode=form_post&response_type=code&redirect_uri=" + [System.Web.HttpUtility]::UrlEncode($RedirectURI)
        $StartLogon = Invoke-WebRequest -uri $AuthURL  -SessionVariable 'AuthSession'
        $Context = [regex]::Match($StartLogon.RawContent,"`"sCtx`":`"(.*?)`"").Groups[1].Value
        $Flow = [regex]::Match($StartLogon.RawContent,"`"sFT`":`"(.*?)`"").Groups[1].Value
        $Canary = [regex]::Match($StartLogon.RawContent,"`"canary`":`"(.*?)`"").Groups[1].Value
        $FBAAuthBody=@{
            "login" = $Credential.UserName
            "loginFmt" = $Credential.UserName
            "i13"="0"
            "type"="11"
            "LoginOptions"="3"
            "passwd"= $Credential.GetNetworkCredential().password.ToString()
            "ps"="2"
            "flowToken"=$Flow
            "canary"=$Canary
            "ctx"=$Context
            "NewUser"="1"
            "fspost"="0"
            "i21"="0"
            "CookieDisclosure"="1"
            "IsFidoSupported"="1"
            "hpgrequestid"=(New-Guid).ToString()
        }
        if (($Context -eq '') -or ($flow -eq '')) { throw "Authorize: $strServiceExceptionMessage. $($StartLogon.RawContent)" }
                
        $FBAResponse = Invoke-WebRequest -Uri https://login.microsoftonline.com/common/login -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FBAAuthBody -WebSession $AuthSession
        
        $formElements = ([XML]$FBAResponse.Content).GetElementsByTagName("input");  
        $authCode = ""
        foreach ($element in $formElements) {
            if ($element.Name -eq "code") {
                $authCode = $element.GetAttribute("value");
                Write-Verbose $authCode
            }
        }  
        

        $Body = @{"grant_type" = "authorization_code"; "scope" = $scopes; "client_id" = "$ClientId"; "code" = $authCode; "redirect_uri" = $RedirectURI }
        $tokenRequest = Invoke-RestMethod -Method Post -ContentType application/x-www-form-urlencoded -Uri https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token -Body $Body 
        return $tokenRequest
    }
} 

<#
    .SYNOPSIS
        Generate a Time-Base One-Time Password based on RFC 6238.
  
    .DESCRIPTION
        This command uses the reference implementation of RFC 6238 to calculate
        a Time-Base One-Time Password. It bases on the HMAC SHA-1 hash function
        to generate a shot living One-Time Password.
  
    .INPUTS
        None.
  
    .OUTPUTS
        System.String. The one time password.
  
    .EXAMPLE
        PS C:\> Get-TimeBasedOneTimePassword -SharedSecret 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        Get the Time-Based One-Time Password at the moment.
  
    .NOTES
        Author : Claudio Spizzi
        License : MIT License
  
    .LINK
        https://github.com/claudiospizzi/SecurityFever
        https://tools.ietf.org/html/rfc6238
#>

function Get-TimeBasedOneTimePassword
{
    [CmdletBinding()]
    [Alias('Get-TOTP')]
    param
    (
        # Base 32 formatted shared secret (RFC 4648).
        [Parameter(Mandatory = $true)]
        [System.String]
        $SharedSecret,

        # The date and time for the target calculation, default is now (UTC).
        [Parameter(Mandatory = $false)]
        [System.DateTime]
        $Timestamp = (Get-Date).ToUniversalTime(),

        # Token length of the one-time password, default is 6 characters.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Length = 6,

        # The hash method to calculate the TOTP, default is HMAC SHA-1.
        [Parameter(Mandatory = $false)]
        [System.Security.Cryptography.KeyedHashAlgorithm]
        $KeyedHashAlgorithm = (New-Object -TypeName 'System.Security.Cryptography.HMACSHA1'),

        # Baseline time to start counting the steps (T0), default is Unix epoch.
        [Parameter(Mandatory = $false)]
        [System.DateTime]
        $Baseline = '1970-01-01 00:00:00',

        # Interval for the steps in seconds (TI), default is 30 seconds.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Interval = 30
    )

    # Generate the number of intervals between T0 and the timestamp (now) and
    # convert it to a byte array with the help of Int64 and the bit converter.
    $numberOfSeconds   = ($Timestamp - $Baseline).TotalSeconds
    $numberOfIntervals = [Convert]::ToInt64([Math]::Floor($numberOfSeconds / $Interval))
    $byteArrayInterval = [System.BitConverter]::GetBytes($numberOfIntervals)
    [Array]::Reverse($byteArrayInterval)

    # Use the shared secret as a key to convert the number of intervals to a
    # hash value.
    $KeyedHashAlgorithm.Key = Convert-Base32ToByte -Base32 $SharedSecret
    $hash = $KeyedHashAlgorithm.ComputeHash($byteArrayInterval)

    # Calculate offset, binary and otp according to RFC 6238 page 13.
    $offset = $hash[($hash.Length-1)] -band 0xf
    $binary = (($hash[$offset + 0] -band '0x7f') -shl 24) -bor
              (($hash[$offset + 1] -band '0xff') -shl 16) -bor
              (($hash[$offset + 2] -band '0xff') -shl 8) -bor
              (($hash[$offset + 3] -band '0xff'))
    $otpInt = $binary % ([Math]::Pow(10, $Length))
    $otpStr = $otpInt.ToString().PadLeft($Length, '0')

    Write-Output $otpStr
}

function Convert-Base32ToByte
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Base32
    )

    # RFC 4648 Base32 alphabet
    $rfc4648 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

    $bits = ''

    # Convert each Base32 character to the binary value between starting at
    # 00000 for A and ending with 11111 for 7.
    foreach ($char in $Base32.ToUpper().ToCharArray())
    {
        $bits += [Convert]::ToString($rfc4648.IndexOf($char), 2).PadLeft(5, '0')
    }

    # Convert 8 bit chunks to bytes, ignore the last bits.
    for ($i = 0; $i -le ($bits.Length - 8); $i += 8)
    {
        [Byte] [Convert]::ToInt32($bits.Substring($i, 8), 2)
    }
}