Api/FlyCommon.ps1

<#
.SYNOPSIS
 
Check if the project name available
 
.DESCRIPTION
 
Check if the project name available
 
.PARAMETER ProjectName
The name of the project which you want to check
 
.OUTPUTS
 
Boolean, True if the project name is available
#>

function Resolve-FlyProjectName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ProjectName}
    )

    Process {
        'Calling method: Resolve-FlyProjectName' | Write-Debug
        $notExist = Invoke-FlyCheckProjectExist -Name $ProjectName
        if (!$notExist) {
            throw ('The project "{0}" already exists. Configure a unique name for the project.' -f $ProjectName)
        }
    }
}

<#
.SYNOPSIS
 
Get the connection by name
 
.DESCRIPTION
 
Get the connection by name
 
.PARAMETER ConnectionName
The name of the connection
 
.OUTPUTS
 
ConnectionSummaryModel, the object model of the connection
#>

function Get-FlyConnectionByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ConnectionName}
    )

    Process {
        'Calling method: Get-FlyConnectionByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $connections = Get-FlyConnections -Search $ConnectionName -Top $top -Skip $skip
            $result = $connections.data | Where-Object { $_.Name -eq $ConnectionName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$connections.nextLink) {
                throw ('Failed to retrieve the connection: {0}. Log in to Fly to confirm the connection name.' -f $ConnectionName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get the policy by name
 
.DESCRIPTION
 
Get the policy by name
 
.PARAMETER PolicyName
The name of the policy
 
.OUTPUTS
 
PolicySummaryModel, the object model of the policy
#>

function Get-FlyPolicyByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${PolicyName},
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${PlatformType}
    )

    Process {
        'Calling method: Get-FlyPolicyByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $policies = Get-FlyPolicies -MigrationModuleType $PlatformType  -Search $PolicyName -Top $top -Skip $skip
            $result = $policies.data | Where-Object { $_.Name -eq $PolicyName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$policies.nextLink) {
                throw ('Failed to retrieve the migration policy: {0}. Log in to Fly to confirm the policy name.' -f $PolicyName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get the tag by name
 
.DESCRIPTION
 
Get the tag by name
 
.PARAMETER TagName
The name of the tag
 
.OUTPUTS
 
TagSummaryModel, the object model of the tag
#>

function Get-FlyTagByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${TagName}
    )

    Process {
        'Calling method: Get-FlyTagByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $tags = Get-FlyTags -Search $TagName -Top $top -Skip $skip
            $result = $tags.data | Where-Object { $_.Name -eq $TagName } | Select-Object -First 1
            if ($null -ne $result) {
                return $result
            }
            if (!$tags.nextLink) {
                throw ('Failed to retrieve the tag: {0}. Log in to Fly to confirm the tag name.' -f $TagName)
            }
            $skip = $skip + $top
        }
    }
}


<#
.SYNOPSIS
 
Get the project by name
 
.DESCRIPTION
 
Get the project by name
 
.PARAMETER ProjectName
The name of the project
 
.PARAMETER PlatformType
The platform of the project
 
.OUTPUTS
 
ProjectSummaryModel, the object model of the project
#>

function Get-FlyProjectByName {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${ProjectName},
        [Parameter(Mandatory = $false)]
        [String]
        [ValidateSet('Exchange', 'Teams', 'SharePoint', 'M365Group', 'TeamChat', 'OneDrive')]
        ${PlatformType}
    )

    Process {
        'Calling method: Get-FlyProjectByName' | Write-Debug
        $top = 200;
        $skip = 0;
        while ($true) {
            $projects = Get-FlyProjects -Search $ProjectName -Top $top -Skip $skip
            $result = $projects.data | Where-Object { $_.Name -eq $ProjectName } | Select-Object -First 1
            if ($null -ne $result) {
                if ($PlatformType -and ($PlatformType -ne [PlatformType].GetEnumName($result.sourcePlatform))) {
                    throw ('The type of the project "{0}" is not {1}.' -f $ProjectName, $PlatformType)
                }
                return $result
            }
            if (!$projects.nextLink) {
                throw ('Failed to retrieve the project: {0}. Log in to Fly to confirm the project name.' -f $ProjectName)
            }
            $skip = $skip + $top
        }
    }
}

<#
.SYNOPSIS
 
Get all project mappings by projectId
 
.DESCRIPTION
 
Get all project mappings by projectId
 
.PARAMETER ProjectName
The GUID of the project
 
.OUTPUTS
 
A list of the ProjectMappingSummaryModel
#>

function Get-FlyAllProjectMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId}
    )

    Process {
        'Calling method: Get-FlyAllProjectMappings' | Write-Debug
        $top = 2000;
        $skip = 0;
        $result = New-Object System.Collections.ArrayList;
        while ($true) {
            $mappings = Get-FlyProjectMappings -ProjectId $ProjectId -Top $top -Skip $skip
            if ($null -ne $mappings.data) {
                $result.AddRange($mappings.data)
            }
            if (!$mappings.nextLink) {
                break;
            }
            $skip = $skip + $top
        }
        return $result
    }
}

function Get-FlySharePointMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlySharePointMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source URL')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination URL')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyExchangeMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyExchangeMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyTeamsMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyTeamsMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Team email address')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Team email address')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyM365GroupMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyM365GroupMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source Group email address')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination Group email address')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyTeamChatMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyTeamChatMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyOneDriveMappings {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${ProjectId},
        [Parameter(Mandatory = $false)]
        [String]
        ${Mappings}
    )

    Process {
        'Calling method: Get-FlyOneDriveMappings' | Write-Debug
        $result = New-Object System.Collections.ArrayList
        if ($Mappings) {
            $allMappings = Get-FlyAllProjectMappings -ProjectId $targetProject.Id
            $targetMappings = Get-FlyMappingsFromCsv -Path $Mappings
            #Match the project mappings between csv file and specified project
            foreach ($target in $targetMappings) {
                foreach ($mapping in $allMappings) {
                    $sourceIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Source user')
                    $destinationIdentity = [System.Web.HttpUtility]::UrlDecode($target.'Destination user')
                    if ($mapping.SourceIdentity -eq $sourceIdentity -and $mapping.DestinationIdentity -eq $destinationIdentity) {
                        [void]$result.Add($mapping);
                        break;
                    }
                }
            }
            if ($result.Count -eq 0) {
                throw 'No mapping in the CSV file matches the existing migration mappings in this project.'
            }
        }
        return $result
    }
}

function Get-FlyMappingsFromCsv {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${Path}
    )
    Process {
        'Calling method: Get-FlyMappingsFromCsv' | Write-Debug
        $isCsv = Confirm-FileExtension -Path $Path -AllowedExtensions 'csv'
        if (-not $isCsv) {
            throw 'The file is not a CSV file.'
        }
        $result = Import-Csv -Path $Path
        if ($null -eq $result) {
            throw 'There is no data configured in the CSV file.'
        }
        return $result
    }
}

function Confirm-FileExtension {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]
        ${Path},
        [Parameter(Mandatory = $true)]
        [String[]]
        ${AllowedExtensions}
    )
    Process {
        'Calling method: Confirm-FileExtension' | Write-Debug
        $extension = ''
        if ($Path) {
            $split = (Split-Path -Path $Path -Leaf).Split(".")
            if ($split -and $split.Count -gt 1) {
                $extension = $split[1]
            }
        }
        if ($AllowedExtensions -contains $extension) {
            return $true
        }
        else {
            return $false
        }
    }
}

function Confirm-PSCustomObjectProperties {
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Object},
        [Parameter(Mandatory = $true)]
        [string[]]
        ${Properties}
    )
    Process {
        'Calling method: Confirm-PSCustomObjectProperties' | Write-Debug
        foreach ($property in $Properties) {
            if (($null -eq $Object.$property) -or ('' -eq $Object.$property)) {
                throw ('{0} cannot be empty.' -f $property) 
            }
        }
    }
}

function Convert-FlyMappingStatus {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]
        ${Mapping}
    )
    Process {
        'Convert-FlyMappingStatus' | Write-Debug
        $progress = $Mapping.JobProgress
        $finalStatus = @([ProjectMappingItemStageStatus]::Successful, [ProjectMappingItemStageStatus]::Exceptioned, [ProjectMappingItemStageStatus]::Failed)
        if ($finalStatus -contains $Mapping.StageStatus) {
            $progress = 100
        }
        return [PSCustomObject]@{
            'Source'           = $Mapping.SourceIdentity
            'Destination'      = $Mapping.DestinationIdentity
            'Stage'            = Get-FlyMappingStage $Mapping.Stage
            'Stage status'     = Get-FlyMappingStageStatus $Mapping
            'Job progress (%)' = $progress
        }
    }
}

function Get-FlyMappingStage {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [ProjectMappingItemStage]
        ${Stage}
    )
    Process {
        'Calling method: Get-FlyMappingStage' | Write-Debug
        switch ($Stage) {
            ([ProjectMappingItemStage]::CreateMapping) { 'New mapping' }
            ([ProjectMappingItemStage]::RunVerification) { 'Mapping verification' }
            ([ProjectMappingItemStage]::RunAssessment) { 'Scan source data' }
            ([ProjectMappingItemStage]::RunDataMigration) { 'Migration' }
            ([ProjectMappingItemStage]::EmailForwarding) { 'Email forwarding' }
            ([ProjectMappingItemStage]::KeepX500EmailAddress) { 'Add X500 email address to destination mailboxes' }
            Default { [ProjectMappingItemStage].GetEnumName($Stage) }
        }
    }
}

function Get-FlyMappingStageStatus {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [PSCustomObject]
        ${Mapping}
    )
    Process {
        'Calling method: Get-FlyMappingStageStatus' | Write-Debug
        if ($Mapping.StageStatus -eq [ProjectMappingItemStageStatus]::Waiting -and $Mapping.ScheduleTime -gt 0) {
            return 'Scheduled'
        }
        else {
            switch ([ProjectMappingItemStageStatus]$Mapping.StageStatus) {
                ([ProjectMappingItemStageStatus]::NotStart) { 'Not started' }
                ([ProjectMappingItemStageStatus]::Waiting) { 'In queue' }
                ([ProjectMappingItemStageStatus]::Queued) { 'In queue with priority' }
                ([ProjectMappingItemStageStatus]::InProgress) { 'In progress' }
                ([ProjectMappingItemStageStatus]::Successful) { 'Finished' }
                ([ProjectMappingItemStageStatus]::Exceptioned) { 'Exceptions' }
                ([ProjectMappingItemStageStatus]::Failed) { 'Failed' }
                ([ProjectMappingItemStageStatus]::Stopped) { 'Stopped' }
                Default { [ProjectMappingItemStageStatus].GetEnumName($Mapping.StageStatus) }
            }
        }
        
    }
}

<#
.SYNOPSIS
 
Convert the string value of object level to integer
 
.DESCRIPTION
 
Convert the string value of object level to integer
 
.PARAMETER Level
The string value of object level
 
.OUTPUTS
 
The integer value of object level
#>

function Get-FlyDataType {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Level}
    )
    Process {
        'Calling method: Get-FlyDataType' | Write-Debug
        switch ($Level) {
            'Site collection' { 600 }
            'Site' { 400 }
            'List' { 200 }
            'Folder' { 100 }
            'User mailbox' { 1001 }
            'Archive mailbox' { 1002 }
            'Distribution list' { 1007 }
            'Microsoft 365 Group mailbox' { 1003 }
            'Resource mailbox' { 1004 }
            'Shared mailbox' { 1005 }
            'Mail-enabled security group' { 1008 }
            'Microsoft 365 Group' { 1006 }
            Default { throw ('Invalid data type') }
        }
    }
}

<#
.SYNOPSIS
 
Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now
 
.DESCRIPTION
 
Convert the string value of method to SharePointMethodTypes, only support SharePoint object for now
 
.PARAMETER Level
The string value of method
 
.OUTPUTS
 
SharePointMethodTypes, refer to SharePointMethodTypes.ps1
#>

function Get-FlySharePointMethod {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Method}
    )
    Process {
        'Calling method: Get-FlySharePointMethod' | Write-Debug
        switch ($Method) {
            'Merge' { [SharePointMethodTypes]::Combine }
            'Attach' { [SharePointMethodTypes]::Attach }
            Default { throw ('Invalid method') }
        }
    }
}

<#
.SYNOPSIS
 
Convert the string value of type to TeamsChannelType
 
.DESCRIPTION
 
Convert the string value of type to TeamsChannelType
 
.PARAMETER Level
The string value of type
 
.OUTPUTS
 
TeamsChannelType, refer to TeamsChannelType.ps1
#>

function Get-FlyTeamsChannelType {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [String]
        ${Type}
    )
    Process {
        'Calling method: Get-FlyTeamsChannelType' | Write-Debug
        switch ($Type) {
            'Standard' { [TeamsChannelType]::Standard }
            'Private' { [TeamsChannelType]::Private }
            'Shared' { [TeamsChannelType]::Shared }
            Default { throw ('Invalid teams channel type') }
        }
    }
}
<#
.SYNOPSIS
 
Output the error details of ErrorRecord
 
.DESCRIPTION
 
Output the error details of ErrorRecord
 
.PARAMETER Level
The information of ErrorRecord
 
.OUTPUTS
 
#>

function ErrorDetail {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]
        ${Error}
    )

    Process {
        'Calling method: ErrorDetail' | Write-Debug
        if ($Error.ErrorDetails.Message) {
            Write-Host $Error.ErrorDetails.Message -ForegroundColor Red
        }
        Write-Error $Error.Exception
    }
}

function Get-IdentityServiceToken {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory)]
        [string]$IdentityServiceUri,
        [Parameter(Mandatory)]
        [string]$Scope,
        [Parameter(Mandatory)]
        [string]$ClientId,
        [Parameter(Mandatory)]
        [Alias("Certificate", "Cert")]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate
    )
    PROCESS {
        'Calling method: Get-IdentityServiceToken' | Write-Debug
        $encodedThumbprint = ConvertTo-Base64UrlEncodedString -Bytes $SigningCertificate.GetCertHash()
        $headerTable = [ordered]@{typ = "JWT"; alg = "RS256"; kid = $encodedThumbprint }
        $header = $headerTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString
        $now = Get-Date
        $currentEpochTime = Convert-DateTimeToEpoch -DateTime $now
        $notBefore = $currentEpochTime
        $futureEpochTime = Convert-DateTimeToEpoch -DateTime ($now.AddHours(1))
        $payloadTable = [ordered]@{sub = $ClientId; jti = ([System.Guid]::NewGuid()).ToString(); iss = $ClientId; aud = $IdentityServiceUri.TrimEnd('/') + "/connect/token"; nbf = $notBefore; exp = $futureEpochTime; iat = $currentEpochTime }
        $payload = $payloadTable | ConvertTo-Json -Compress | ConvertTo-Base64UrlEncodedString
        $jwtPlainText = "{0}.{1}" -f $header, $payload
        $jwtSig = New-JwtRsaSignature -JsonWebToken $jwtPlainText -SigningCertificate $SigningCertificate
        $ClientAssertion = "{0}.{1}" -f $jwtPlainText, $jwtSig
        $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token"
        $Body = @{
            grant_type            = 'client_credentials'
            scope                 = $Scope
            client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
            client_assertion      = $ClientAssertion
        }
        $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing
        return (ConvertFrom-Json $Response).access_token
    }
}

function Get-IdentityServiceTokenByClientSecret {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory)]
        [string]$IdentityServiceUri,
        [Parameter(Mandatory)]
        [string]$Scope,
        [Parameter(Mandatory)]
        [string]$ClientId,
        [Parameter(Mandatory)]
        [string]$ClientSecret
    )
    PROCESS {
        'Calling method: Get-IdentityServiceTokenByClientSecret' | Write-Debug
        $RequestUri = $IdentityServiceUri.TrimEnd('/') + "/connect/token"
        $Body = @{
            grant_type    = 'client_credentials'
            scope         = $Scope
            client_id     = $ClientId
            client_secret = $ClientSecret
        }
        $Response = Invoke-WebRequest -Uri $RequestUri -Method 'POST' -Body $Body -ErrorAction Stop -UseBasicParsing
        return (ConvertFrom-Json $Response).access_token
    }
}

function New-JwtRsaSignature {
    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$SigningCertificate,
        [String]$JsonWebToken
    )
    PROCESS {
        'Calling method: New-JwtRsaSignature' | Write-Debug
        $rsa = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($SigningCertificate))
        if ($null -eq $rsa) {
            # Requiring the private key to be present
            throw "There's no private key in the supplied certificate." 
        }
        [byte[]]$message = [System.Text.Encoding]::UTF8.GetBytes($JsonWebToken)
        $sigBytes = $rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
        return ConvertTo-Base64UrlEncodedString -Bytes $sigBytes
    }
}


function ConvertTo-Base64UrlEncodedString {
    [CmdletBinding()]
    [OutputType([string])]
    Param (
        [Parameter(Position = 0, ParameterSetName = "String", Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$InputString,

        [Parameter(Position = 1, ParameterSetName = "Byte Array", Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)]
        [byte[]]$Bytes
    )
    PROCESS {
        [string]$base64UrlEncodedString = ""

        if ($PSBoundParameters.ContainsKey("Bytes")) {
            $output = [Convert]::ToBase64String($Bytes)
            $output = $output.Split('=')[0] # Remove any trailing '='s
            $output = $output.Replace('+', '-') # 62nd char of encoding
            $output = $output.Replace('/', '_') # 63rd char of encoding
            $base64UrlEncodedString = $output

        }
        else {
            $encoder = [System.Text.UTF8Encoding]::new()
            [byte[]]$inputBytes = $encoder.GetBytes($InputString)
            $base64String = [Convert]::ToBase64String($inputBytes)
            [string]$base64UrlEncodedString = ""
            $base64UrlEncodedString = $base64String.Split('=')[0] # Remove any trailing '='s
            $base64UrlEncodedString = $base64UrlEncodedString.Replace('+', '-'); # 62nd char of encoding
            $base64UrlEncodedString = $base64UrlEncodedString.Replace('/', '_'); # 63rd char of encoding
        }
        return $base64UrlEncodedString
    }
}

function Convert-DateTimeToEpoch {
    [CmdletBinding()]
    [OutputType([System.Int64])]
    Param(
        [Parameter(Mandatory)]
        [DateTime]$DateTime
    )
    PROCESS {
        'Calling method: Convert-DateTimeToEpoch' | Write-Debug
        $dtut = $DateTime.ToUniversalTime()
        [TimeSpan]$ts = New-TimeSpan -Start  (Get-Date "01/01/1970") -End $dtut
        [Int64]$secondsSinceEpoch = [Math]::Floor($ts.TotalSeconds)
        return $secondsSinceEpoch
    }
}

function Convert-JWTtoken {
    [CmdletBinding()]
    [OutputType([System.Int64])]
    Param(
        [Parameter(Mandatory = $true)]
        [String]$token
    )

    PROCESS {
        'Calling method: Convert-JWTtoken' | Write-Debug
        #Validate as per https://tools.ietf.org/html/rfc7519
        #Access and ID tokens are fine, Refresh tokens will not work
        if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { Write-Error "Invalid token" -ErrorAction Stop }
 
        #Header
        $tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/')
        #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
        while ($tokenheader.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenheader += "=" }

        #Payload
        $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/')
        #Fix padding as needed, keep adding "=" until string length modulus 4 reaches 0
        while ($tokenPayload.Length % 4) { Write-Verbose "Invalid length for a Base-64 char array or string, adding ="; $tokenPayload += "=" }

        #Convert to Byte array
        $tokenByteArray = [System.Convert]::FromBase64String($tokenPayload)
        #Convert to string array
        $tokenArray = [System.Text.Encoding]::UTF8.GetString($tokenByteArray)

        #Convert from JSON to PSObject
        $result = $tokenArray | ConvertFrom-Json
        return $result
    }
}