PSMSALNet.psm1

#Region '.\prefix.ps1' 0
# This is required to expose in a private way the cmdlet Get-WAMToken which is required when we use the -WAMFlow
import-module $(Join-Path $PSScriptRoot 'lib' 'WAMHelper.dll') -ErrorAction Stop
#EndRegion '.\prefix.ps1' 3
#Region '.\Public\ConvertTo-X509Certificate2.ps1' 0
function ConvertTo-X509Certificate2
{
    <#
.SYNOPSIS
This function will output a X509Certificate2 certificate.
.DESCRIPTION
Because Linux does not have the Cert: provider (Get-PsProvider), we have to find a way to use the provided certificate on all platforms and X509
is a solution. This function will inject several format (cer,crt,pem,pfx) to expose the result in a standardized way.
.PARAMETER PfxPath
Specify path of a pfx file.
.PARAMETER PemPath
Specify path of a pem file.
.PARAMETER CerPath
Specify path of a cer file.
.PARAMETER CrtPath
Specify path of a crt file.
.PARAMETER Password
Specify password of a pfx file.
.PARAMETER PrivateKeyPath
Specify path of a decrypted private key.
.PARAMETER KeyVaultCertificatePath
Specify path of a specific certificate version hosted on Azure Key Vault.
.PARAMETER AccessToken
Specify an access token to contact the associated Key Vault.
.PARAMETER APIVersion
Specify the API version regarding Keyvault API for now the default value is 7.3.
.PARAMETER ExportPrivateKey
Specify you want to extract from Key Vault the certificate with the Private key.
.EXAMPLE
 
$PubCert = ConvertTo-X509Certificate2 -CerPath ./scomnewbie.cer
 
Will generate a X509Certificate2 without private key from a cer file.
 
.EXAMPLE
 
$PubCert = ConvertTo-X509Certificate2 -CerPath ./scomnewbie.crt
 
Will generate a X509Certificate2 without private key from a crt file.
 
.EXAMPLE
 
$PrivCert = ConvertTo-X509Certificate2 -PfxPath ./scomnewbie.pfx -Password $(ConvertTo-SecureString -String "exportpassword" -AsPlainText -Force)
$PrivCert.PrivateKey
 
Will generate a X509Certificate2 with private key from a pfx file.
 
.EXAMPLE
 
$PrivCert = ConvertTo-X509Certificate2 -PemPath ./scomnewbie2.pem -PrivateKeyPath ./privatekey_rsa.key
$PrivCert.PrivateKey
 
Will generate a X509Certificate2 with private key from a pem file.
 
.EXAMPLE
 
$CertURL = 'https://<myvault>.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb'
$KVToken = (Get-AzAccessToken -Resource "https://vault.azure.net").Token #Once authenticated to Azure
ConvertTo-X509Certificate2 -KeyVaultCertificatePath $CertURL -AccessToken $KVToken
 
Will generate a X509Certificate2 with public key from a certificate hosted in Key Vault.
.EXAMPLE
 
$CertURL = 'https://<myvault>.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb'
$KVToken = (Get-AzAccessToken -Resource "https://vault.azure.net").Token #Once authenticated to Azure
ConvertTo-X509Certificate2 -KeyVaultCertificatePath $CertURL -AccessToken $KVToken -ExportPrivateKey
 
Will generate a X509Certificate2 with private key from a certificate hosted in Key Vault.
 
.NOTES
VERSION HISTORY
1.0 | 2023/10/03 | Francois LEON
    initial version
POSSIBLE IMPROVEMENT
    -
#>

    [CmdletBinding()]
    [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])]
    #[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","")]
    param(
        [parameter(Mandatory, ParameterSetName = 'pfx')]
        [ValidateScript({
                if ((Test-Path $_) -AND ($_ -like '*.pfx'))
                {
                    $true
                }
                else
                {
                    throw "Path $_ is not valid"
                }
            })]
        [String]$PfxPath,
        [parameter(Mandatory, ParameterSetName = 'pem')]
        [ValidateScript({
                if ((Test-Path $_) -AND ($_ -like '*.pem'))
                {
                    $true
                }
                else
                {
                    throw "Path $_ is not valid"
                }
            })]
        [String]$PemPath,
        [parameter(Mandatory, ParameterSetName = 'crt')]
        [ValidateScript({
                if ((Test-Path $_) -AND ($_ -like '*.crt'))
                {
                    $true
                }
                else
                {
                    throw "Path $_ is not valid"
                }
            })]
        [String]$CrtPath,
        [parameter(Mandatory, ParameterSetName = 'cer')]
        [ValidateScript({
                if ((Test-Path $_) -AND ($_ -like '*.cer'))
                {
                    $true
                }
                else
                {
                    throw "Path $_ is not valid"
                }
            })]
        [String]$CerPath,
        [parameter(ParameterSetName = 'pfx')]
        [ValidateScript({
                if ($_.Length -gt 0)
                {
                    $true
                }
                else
                {
                    throw 'SecureString argument contained no data.'
                }
            })]
        [securestring]$Password,
        [parameter(ParameterSetName = 'pem')]
        [ValidateScript({
                if ((Test-Path $_) -AND ( $(Get-Content $_ | Select-Object -First 1) -eq '-----BEGIN PRIVATE KEY-----' ))
                {
                    $true
                }
                else
                {
                    throw "Path $_ is not valid or private key is not visible"
                }
            })]
        [string]$PrivateKeyPath,
        [parameter(Mandatory, ParameterSetName = 'keyvault')]
        [string]$KeyVaultCertificatePath, #https://ubuntukv415745.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb
        [parameter(Mandatory, ParameterSetName = 'keyvault')]
        [string]$AccessToken, #(Get-AzAccessToken -Resource "https://vault.azure.net").Token
        [parameter(ParameterSetName = 'keyvault')]
        [string]$APIVersion = '7.3',
        [parameter(ParameterSetName = 'keyvault')]
        [switch]$ExportPrivateKey
    )

    Begin
    {
        Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)"

        # Just keep what we need to avoid useless switch iteration
        $PSBoundParameters.Remove('KeyVaultAccessToken') | Out-Null
        $PSBoundParameters.Remove('ExportPrivateKey') | Out-Null
        $PSBoundParameters.Remove('PrivateKeyPath') | Out-Null
        $PSBoundParameters.Remove('Password') | Out-Null
        $PSBoundParameters.Remove('AccessToken') | Out-Null
        $PSBoundParameters.Remove('APIVersion') | Out-Null
        $PSBoundParameters.Remove('ExportPrivateKey') | Out-Null

    } #begin

    Process
    {
        switch ($PSBoundParameters.Keys)
        {

            'CerPath'
            {
                #Even if it's not the same format crt and cer is using the same method. I will duplicate code for readability.
                [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($(Get-Item -Path $CerPath))
                break
            }

            'CrtPath'
            {
                [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($(Get-Item -Path $CrtPath))
                break
            }

            'PfxPath'
            {
                if ($Password)
                {
                    #Means private key protected by password
                    # Means Linux/Windows/MacOS running on Powershell 7 (Yes v6 does not count :D)
                    [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($(Get-Item -Path $PfxPath), $(ConvertFrom-SecureString -SecureString $Password -AsPlainText))
                    break
                }
                else
                {
                    #Means no password to protect the private key
                    [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($(Get-Item -Path $PfxPath))
                    break
                }
            }

            'PemPath'
            {
                if ($PrivateKeyPath)
                {
                    #Means private key protected by password
                    #openssl pkcs12 -in ./scomnewbie.pfx -out ./scomnewbie2.pem # Privatekey will be encrypted + no -nodes means passphrase required
                    #openssl rsa -in ./scomnewbie2.pem -out privatekey_rsa.key #Enter passphrase + decode PK
                    [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPemFile($(Get-Item -Path $PemPath), $(Get-Item -Path $PrivateKeyPath))
                    break
                }
                else
                {
                    #Means no password to protect the private key
                    #openssl pkcs12 -in ./scomnewbie.pfx -out ./scomnewbie.pem -nodes # WARNING No more password anymore + PK decoded
                    if ($(Get-Content -Path $PemPath) -match '-----BEGIN PRIVATE KEY-----')
                    {
                        [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPemFile($(Get-Item -Path $PemPath)) # Make sure private key is not encrypted!
                        break
                    }
                    else
                    {
                        throw "Make sure you're private key is not encrypted"
                    }
                }
            }

            'KeyVaultCertificatePath'
            {
                if ($ExportPrivateKey)
                {
                    $CertInfo = Get-KVCertificateWithPrivateKey -KeyVaultCertificatePath $KeyVaultCertificatePath -AccessToken $AccessToken -APIVersion $APIVersion
                    $pfxUnprotectedBytes = [Convert]::FromBase64String($CertInfo.value)
                    [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxUnprotectedBytes)
                }
                else
                {
                    $CertInfo = Get-KVCertificateWithPublicKey -KeyVaultCertificatePath $KeyVaultCertificatePath -AccessToken $AccessToken -APIVersion $APIVersion
                    if ($IsWindows)
                    {
                        $cBytes = [System.Text.Encoding]::UTF8.GetBytes($CertInfo.cer)
                        [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($cBytes)
                    }
                    else
                    {
                        $ModCertInfo = @"
-----BEGIN CERTIFICATE-----
$($CertInfo.cer)
-----END CERTIFICATE-----
"@

                        $cBytes = [System.Text.Encoding]::UTF8.GetBytes($ModCertInfo)
                        [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($cBytes)
                    }
                }
            }
        }#end switch
    } #process

    End
    {
        Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)"
    } #end
}
#EndRegion '.\Public\ConvertTo-X509Certificate2.ps1' 275
#Region '.\Public\Get-EntraToken.ps1' 0
function Get-EntraToken {
    <#
    .SYNOPSIS
    This function will interact with MSAL.
    .DESCRIPTION
    Use this function to generate JWT Entra tokens. By default the token cache will be in memory.
    .PARAMETER ClientCredentialFlowWithSecret
    The ClientCredentialFlowWithSecret parameter defines you want to generate an entra token with the client credential flow with secrets.
    .PARAMETER ClientCredentialFlowWithCertificate
    The ClientCredentialFlowWithCertificate parameter defines you want to generate an entra token with the client credential flow with certificates.
    .PARAMETER PublicAuthorizationCodeFlow
    The PublicAuthorizationCodeFlow parameter defines you want to generate an entra token with the Authorization Code flow with PKCE. No secrets are required.
    .PARAMETER DeviceCodeFlow
    The DeviceCodeFlow parameter defines you want to generate an entra token with the Device code flow. No secrets are required.
    .PARAMETER WAMFlow
    The WAMFlow parameter defines you want to generate an entra token with the Windows WAM (Web Account Manager). No secrets are required.
    .PARAMETER OnBehalfFlowWithSecret
    The OnBehalfFlowWithSecret parameter defines you want to generate an entra token with the On behalf flows with secrets.
    .PARAMETER OnBehalfFlowWithCertificate
    The OnBehalfFlowWithCertificate parameter defines you want to generate an entra token with the On behalf flows with certificates.
    .PARAMETER FederatedCredentialFlowWithAssertion
    The FederatedCredentialFlowWithAssertion parameter defines you want to generate an entra token with the federated credential authentication method. A token assertion is required.
    .PARAMETER UserAssertion
    The UserAssertion parameter defines the token you want to use in both the OBO flow or the federated credential flow.
    .PARAMETER SystemManagedIdentity
    The SystemManagedIdentity parameter defines the token you want to generate an entra token with the system managed identity.
    .PARAMETER UserManagedIdentity
    The UserManagedIdentity parameter defines the token you want to generate an entra token with the user managed identity.
    .PARAMETER ClientId
    The ClientId parameter defines the client Id (application Id) you want to use to generate a token.
    .PARAMETER ClientSecret
    The ClientSecret parameter defines the client secret you want to use in your authentication flow.
    .PARAMETER ClientCertificate
    The ClientCertificate parameter defines the client certificate you want to use in your authentication flow.
    .PARAMETER WithoutCaching
    The WithoutCaching parameter defines the you want to force a token refresh instead of using the MSAL cache.
    .PARAMETER AzureCloudInstance
    The AzureCloudInstance parameter defines the Azure environment you plan to consume. By default, the module target Azure public.
    .PARAMETER TenantId
    The TenantId parameter defines the Entra tenant Id. If you don't specificy this parameter, the common authority will be used (multi-tenants applications)
    .PARAMETER RedirectUri
    The RedirectUri parameter defines the redirect uri required for several public workflow. By default this parameter equal http://localhost.
    .PARAMETER Resource
    The Resource parameter defines the resource you want to consume. A scope is composed of a resource and a permission. This parameter is pre-filled with Azure audiences like Graph API, KeyVault or Custom (your API)
    .PARAMETER CustomResource
    The CustomResource parameter defines your custom resource you exposed in Entra (api://<...>). You have to use it in addition of the Custom Resource parameter value.
    .PARAMETER Permissions
    The Permissions parameter defines the permissions you request. This is usually what is after api://<...>/<Permission> or with Graph API @("User.Read","Group.Read"). the combinaison of Resource and permission create the scope.
    .PARAMETER ExtraScopesToConsent
    The ExtraScopesToConsent parameter defines the extra scopes you need following the Entra limitation where you can call only one resource per call. Thi parameter is useful when you need Graph API and ARM token.
    .PARAMETER WithDebugLogging
    The WithDebugLogging enable the MSAL library logging.
    .EXAMPLE
 
    $HashArguments = @{
        ClientId = $clientId
        ClientSecret = $ClientSecret
        TenantId = $TenantId
        Resource = 'GraphAPI'
    }
    Get-EntraToken -ClientCredentialFlowWithSecret @HashArguments
 
    This command will generate a token to access Graph API scope with all application permissions assign to this app registration. The token is stored in memory cache managed by MSAL.
    .EXAMPLE
 
    $HashArguments = @{
        ClientId = $clientId
        TenantId = $TenantId
        RedirectUri = 'http://localhost'
        Resource = 'GraphAPI'
        Permissions = @('user.read','group.read.all')
        ExtraScopesToConsent = @('https://management.azure.com/user_impersonation')
        verbose = $true
    }
 
    Get-EntraToken -PublicAuthorizationCodeFlow @HashArguments
 
    This command will generate a token to access Graph API scope with all application permissions added in the request. In addition, the request will do a second call to Entra to generate a token to access the ARM resource. The token is stored in memory cache managed by MSAL.
    .EXAMPLE
 
    Get-EntraToken -DeviceCodeFlow -ClientId $ClientId -TenantId $TenantId -Resource GraphAPI -Permissions @('user.read')
 
    This command will generate a token to access Graph API (user.read) scope with the default redirect uri value which is 'http://localhost'
    .EXAMPLE
 
    Get-EntraToken -WAMFlow -ClientId $clientId -TenantId $tenantId -RedirectUri 'ms-appx-web://Microsoft.AAD.BrokerPlugin/9f0...8f01' -Resource Custom -CustomResource api://AADToken-WebAPI-back-OBO -Permissions access_asuser
 
    This command (Windows only) use the Web Account MAnager component to communicate with Entra. In this case we request a token to access a custom API protected by entra. The redirect uri is not configured to use localhost as usual.
    .EXAMPLE
 
    $X509 = ConvertTo-X509Certificate2 -PfxPath C:\TEMP\newcert.pfx -Password $(ConvertTo-SecureString -String '{myPassword}' -AsPlainText -Force) -Verbose
    Get-EntraToken -OnBehalfFlowWithCertificate -ClientCertificate $X509 -UserAssertion $FrontEndClientToken.accesstoken -ClientId $BackendClientId -TenantId $tenantId -Resource GraphAPI -Permissions 'User.read' | % AccessToken
 
    This command, executed from a backend api will generate a tokens using the OBO flow with certificate.
    .EXAMPLE
 
    $KubeSaToken = Get-Content -Path '/var/run/secrets/azure/tokens/azure-identity-token'
    Get-EntraToken -FederatedCredentialFlowWithAssertion -UserAssertion $KubeSaToken -ClientId $([Environment]::GetEnvironmentVariable('AZURE_CLIENT_ID')) -TenantId $([Environment]::GetEnvironmentVariable('AZURE_TENANT_ID')) -Resource GraphAPI
 
    This command will generate a token to access Graph API (/.default) scope from a Kubernetes pod.
    .NOTES
    VERSION HISTORY
    2023/09/23 | Francois LEON
        initial version
    #>

    [cmdletbinding()]
    [OutputType([Microsoft.Identity.Client.AuthenticationResult])]
    param
    (
        # Identifier of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowSecret')]
        [switch]$ClientCredentialFlowWithSecret,

        # Identifier of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowCertificate')]
        [switch]$ClientCredentialFlowWithCertificate,

        [Parameter(Mandatory, ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [switch]$PublicAuthorizationCodeFlow,

        [Parameter(Mandatory, ParameterSetName = 'DeviceCodeFlow')]
        [switch]$DeviceCodeFlow,

        [Parameter(Mandatory, ParameterSetName = 'WAMFlow')]
        [switch]$WAMFlow,

        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [switch]$OnBehalfFlowWithSecret,

        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [switch]$OnBehalfFlowWithCertificate,

        [Parameter(Mandatory, ParameterSetName = 'FederatedCredentialFlowWithAssertion')]
        [switch]$FederatedCredentialFlowWithAssertion,

        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [Parameter(Mandatory, ParameterSetName = 'FederatedCredentialFlowWithAssertion')]
        [string]$UserAssertion,

        # Identifier of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'SystemManagedIdentity')]
        [switch]$SystemManagedIdentity,

        # Identifier of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'UserManagedIdentity')]
        [switch]$UserManagedIdentity,

        # Identifier of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowSecret')]
        [Parameter(Mandatory, ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [Parameter(Mandatory, ParameterSetName = 'UserManagedIdentity')]
        [Parameter(Mandatory, ParameterSetName = 'DeviceCodeFlow')]
        [Parameter(Mandatory, ParameterSetName = 'WAMFlow')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [Parameter(Mandatory, ParameterSetName = 'FederatedCredentialFlowWithAssertion')]
        [guid] $ClientId,

        # Secure secret of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowSecret')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [string] $ClientSecret,

        # Client assertion certificate of the client requesting the token.
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [System.Security.Cryptography.X509Certificates.X509Certificate2] $ClientCertificate,

        # Will generate a new token on each call with this param
        [Parameter(ParameterSetName = 'ClientCredentialFlowSecret')]
        [Parameter(ParameterSetName = 'ClientCredentialFlowCertificate')]
        [Parameter(ParameterSetName = 'FederatedCredentialFlowWithAssertion')]
        [switch] $WithoutCaching,

        # Instance of Azure Cloud
        [ValidateSet('AzurePublic','AzureChina','AzureUsGovernment','AzureGermany')]
        [Microsoft.Identity.Client.AzureCloudInstance] $AzureCloudInstance = 'AzurePublic',

        # Tenant identifier of the authority to issue token. It can also contain the value "consumers" or "organizations".
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'ClientCredentialFlowSecret')]
        [Parameter(ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [Parameter(ParameterSetName = 'DeviceCodeFlow')]
        [Parameter(ParameterSetName = 'WAMFlow')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [Parameter(Mandatory, ParameterSetName = 'FederatedCredentialFlowWithAssertion')]
        [guid] $TenantId,

        # Address to return to upon receiving a response from the authority.
        [Parameter(ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [Parameter(ParameterSetName = 'DeviceCodeFlow')]
        [Parameter(ParameterSetName = 'WAMFlow')]
        [uri] $RedirectUri = 'http://localhost',

        #Scope = Resource + Permission
        [parameter(Mandatory)]
        [ValidateSet('Keyvault','ARM','GraphAPI','Storage','Monitor', 'LogAnalytics', 'PostGreSql','Custom')] #TODO: valider Graph API not sure it's working
        [string] $Resource,

        [string] $CustomResource = $null, #https:// ... should be used only with Custom Audience like api://<your api>

        [Parameter(Mandatory, ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [Parameter(Mandatory, ParameterSetName = 'DeviceCodeFlow')]
        [Parameter(Mandatory, ParameterSetName = 'WAMFlow')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithCertificate')]
        [Parameter(Mandatory, ParameterSetName = 'OnBehalfFlowWithSecret')]
        [string[]] $Permissions, #User.read, Directory.Read ...

        [Parameter(ParameterSetName = 'PublicAuthorizationCodeFlow')]
        [Parameter(ParameterSetName = 'WAMFlow')]
        [Parameter(ParameterSetName = 'DeviceCodeFlow')]
        [string[]]$ExtraScopesToConsent,

        [switch]$WithDebugLogging
    )

    Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)"

    if ($Resource -eq 'Custom') {
        if ($null -eq $CustomResource) {
            Throw "CustomScope parameter should not be null when you're using Custom audience"
        }
    }

    switch ($Resource) {
        'Keyvault' { $ScopesUri = 'https://vault.azure.net';break }
        'ARM' { $ScopesUri = 'https://management.azure.com';break }
        'GraphAPI' { $ScopesUri = 'https://graph.microsoft.com';break }
        'Storage' { $ScopesUri = 'https://storage.azure.com';break }
        'Monitor' { $ScopesUri = 'https://monitor.azure.com';break }
        'LogAnalytics' { $ScopesUri = 'https://api.loganalytics.io';break }
        'PostGreSql' { $ScopesUri = 'https://ossrdbms-aad.database.windows.net';break }
        default { $ScopesUri = $CustomResource }
    }

    If ($PSBoundParameters[@('ClientCredentialFlowWithSecret','ClientCredentialFlowWithCertificate','SystemManagedIdentity','UserManagedIdentity','FederatedCredentialFlowWithAssertion')]) {
        # In case a user provide api://fsdfsdf/ with a / at the end
        if($CustomResource -match '\\$'){
            [string[]]$scopes = '{0}{1}' -f $ScopesUri,'.default'
        }
        else{
            [string[]]$scopes = '{0}/{1}' -f $ScopesUri,'.default'
        }
    }
    else{
        # Means user may provide one Resource (Azure limitation) but multiple permissions
        $TempArray = @()
        Foreach($Permission in $Permissions){
            if($CustomResource -match '\\$'){
                $TempArray += '{0}{1}' -f $ScopesUri,$Permission
            }
            else{
                $TempArray += '{0}/{1}' -f $ScopesUri,$Permission
            }
        }
        [string[]]$scopes = $TempArray
    }

    Write-Verbose "[$((Get-Date).TimeofDay)] Scope requested are $Scopes"

    #Reset main variables
    [Microsoft.Identity.Client.AuthenticationResult] $AuthenticationResult = $T = $WAMToken = $null

    # This is the memory cache MSAL will use
    if(-not (Get-variable -Name PublicClientApplications -ErrorAction SilentlyContinue)){
        [System.Collections.Generic.List[Microsoft.Identity.Client.IPublicClientApplication]] $script:PublicClientApplications = New-Object 'System.Collections.Generic.List[Microsoft.Identity.Client.IPublicClientApplication]'
    }

    If ($PSBoundParameters[@('ClientCredentialFlowWithSecret','ClientCredentialFlowWithCertificate','OnBehalfFlowWithSecret','OnBehalfFlowWithCertificate','FederatedCredentialFlowWithAssertion')]) {
        Write-Verbose "[$((Get-Date).TimeofDay)] Confidential application selected"
        $ClientApplicationBuilder = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientId)
        #Common Authority can't be used with this flow
        $ClientApplicationBuilder.WithAuthority($AzureCloudInstance,$TenantId) | Out-Null
        if($WithoutCaching){
            Write-Verbose "[$((Get-Date).TimeofDay)] Caching disabled with client credential flow"
            $ClientApplicationBuilder.WithCacheOptions($false) | Out-Null
        }
        else{
            $ClientApplicationBuilder.WithCacheOptions($true) | Out-Null
        }

        switch -regex ($PSBoundParameters.Keys) {
            'ClientCredentialFlowWithSecret|OnBehalfFlowWithSecret' {
                $ClientApplicationBuilder.WithClientSecret($ClientSecret) | Out-Null
                break
            }
            'ClientCredentialFlowWithCertificate|OnBehalfFlowWithCertificate' {
                $ClientApplicationBuilder.WithCertificate($ClientCertificate) | Out-Null
                break
            }
            'FederatedCredentialFlowWithAssertion'{
                #https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/web-apps-apis/confidential-client-assertions
                $ClientApplicationBuilder.WithClientAssertion($UserAssertion) | Out-Null
                break
            }
            default {
                throw 'Should not go there'
            }
        }

    }
    elseif ($PSBoundParameters['SystemManagedIdentity']) {
        Write-Verbose "[$((Get-Date).TimeofDay)] System Managed identity selected"
        $ClientApplicationBuilder = [Microsoft.Identity.Client.ManagedIdentityApplicationBuilder]::Create([Microsoft.Identity.Client.AppConfig.ManagedIdentityId]::SystemAssigned)
    }
    elseif ($PSBoundParameters['UserManagedIdentity']) {
        Write-Verbose "[$((Get-Date).TimeofDay)] User Managed identity selected"
        $ClientApplicationBuilder = [Microsoft.Identity.Client.ManagedIdentityApplicationBuilder]::Create([Microsoft.Identity.Client.AppConfig.ManagedIdentityId]::WithUserAssignedClientId($ClientId))
    }
    else {
        # used by authorizationCode & Device code
        Write-Verbose "[$((Get-Date).TimeofDay)] Public application selected"
        $ClientApplicationBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId)
        if ($PSBoundParameters['TenantId']) {
            Write-Verbose "[$((Get-Date).TimeofDay)] Single tenant app used"
            $ClientApplicationBuilder.WithAuthority($AzureCloudInstance,$TenantId) | Out-Null
        }
        else {
            Write-Verbose "[$((Get-Date).TimeofDay)] Multi tenant app used"
            $ClientApplicationBuilder.WithAuthority($AzureCloudInstance,'common') | Out-Null
        }

        if($WAMFlow){
            Write-Verbose "[$((Get-Date).TimeofDay)] WAM flow selected"
            #Never succeed to make WAM working straight on pwsh. The method WithBroker does not work.
            if($TenantId){
                Write-Verbose "[$((Get-Date).TimeofDay)] Single tenant app used"
                if($extraScopesToConsent){
                    $WAMToken = Get-WAMToken -ClientId $ClientId -RedirectUri $RedirectUri -TenantId $TenantId -AzureCloudInstance $AzureCloudInstance -Scopes $Scopes -extraScopesToConsent $ExtraScopesToConsent
                }
                else{
                    $WAMToken = Get-WAMToken -ClientId $ClientId -RedirectUri $RedirectUri -TenantId $TenantId -AzureCloudInstance $AzureCloudInstance -Scopes $Scopes
                }
            }
            else{
                # Will use the common endpoint
                Write-Verbose "[$((Get-Date).TimeofDay)] Multi tenant app used"
                if($extraScopesToConsent){
                    $WAMToken = Get-WAMToken -ClientId $ClientId -RedirectUri $RedirectUri -AzureCloudInstance $AzureCloudInstance -Scopes $Scopes -extraScopesToConsent $ExtraScopesToConsent
                }
                else{
                    $WAMToken = Get-WAMToken -ClientId $ClientId -RedirectUri $RedirectUri -AzureCloudInstance $AzureCloudInstance -Scopes $Scopes
                }
            }

            return $WAMToken
            #https://devblogs.microsoft.com/identity/improved-windows-broker-support-with-msal-net/
        }
        else{
            $ClientApplicationBuilder.WithRedirectUri($RedirectUri) | Out-Null
        }
    }

    if($WithDebugLogging){
        Write-Verbose "[$((Get-Date).TimeofDay)] Debug logging selected"
        #https://learn.microsoft.com/en-us/entra/msal/dotnet/advanced/exceptions/msal-logging#logging-levels
        $ClientApplicationBuilder.WithLogging([PSMSALNetHelper.Mylogger]::new(), 'piilogging')  | Out-Null
    }

    $ClientApplication = $ClientApplicationBuilder.Build()

    If ($PSBoundParameters[@('ClientCredentialFlowWithSecret','ClientCredentialFlowWithCertificate','FederatedCredentialFlowWithAssertion')]) {
        #Client credential flow no user cache so no silent
        $AquireTokenParameters = $ClientApplication.AcquireTokenForClient($Scopes)
        $ClientApplication.AcquireTokenForClien
    }
    elseif($PSBoundParameters[@('SystemManagedIdentity','UserManagedIdentity')]){
        $AquireTokenParameters = $ClientApplication.AcquireTokenForManagedIdentity($Scopes)
    }
    elseif($PSBoundParameters[@('OnBehalfFlowWithCertificate','OnBehalfFlowWithSecret')]){
        $AquireTokenParameters = $ClientApplication.AcquireTokenOnBehalfOf($Scopes, [Microsoft.Identity.Client.UserAssertion]::new($UserAssertion))
    }
    else{
        try{

            $T = $PublicClientApplications | Where-Object { $_.ClientId -eq $ClientId -and $_.AppConfig.RedirectUri -eq $RedirectUri} | Select-Object -Last 1
            if($null -eq $T){
                $PublicClientApplications.Add($ClientApplication)
            }
            else{
                $ClientApplication = $T
            }

            [Microsoft.Identity.Client.IAccount]$Account = $ClientApplication.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1
            if($null -eq $Account){
                throw
            }else{
                $Account
            }
            Write-Verbose "[$((Get-Date).TimeofDay)] Acquire token silently"
            $AquireTokenParameters = $ClientApplication.AcquireTokenSilent($Scopes, $Account)
        }
        catch{
            if($DeviceCodeFlow){
                Write-Verbose "[$((Get-Date).TimeofDay)] Acquire token with device code"
                $AquireTokenParameters = $ClientApplication.AcquireTokenWithDeviceCode($Scopes, [DeviceCodeHelper]::GetDeviceCodeResultCallback())
            }
            else{
                Write-Verbose "[$((Get-Date).TimeofDay)] Acquire token interactively"
                $AquireTokenParameters = $ClientApplication.AcquireTokenInteractive($Scopes)
                if($extraScopesToConsent){
                    $AquireTokenParameters.WithExtraScopesToConsent($extraScopesToConsent) | Out-Null
                }
            }

        }
    }

    # Do the async call to get a token
    $Timeout = New-TimeSpan -Minutes 2
    $tokenSource = New-Object System.Threading.CancellationTokenSource
    try {
        #$AuthenticationResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult()
        $taskAuthenticationResult = $AquireTokenParameters.ExecuteAsync($tokenSource.Token)
        try {
            $endTime = [datetime]::Now.Add($Timeout)
            while (!$taskAuthenticationResult.IsCompleted) {
                if ($Timeout -eq [timespan]::Zero -or [datetime]::Now -lt $endTime) {
                    Start-Sleep -Seconds 1
                }
                else {
                    $tokenSource.Cancel()
                    $taskAuthenticationResult.Wait()
                    #try { $taskAuthenticationResult.Wait() }
                    #catch { }
                    Write-Error -Exception (New-Object System.TimeoutException) -Category ([System.Management.Automation.ErrorCategory]::OperationTimeout) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureOperationTimeout' -TargetObject $AquireTokenParameters -ErrorAction Stop
                }
            }
        }
        finally {
            if (!$taskAuthenticationResult.IsCompleted) {
                Write-Warning 'Canceling Token Acquisition for Application with ClientId [{0}]' -f $ClientApplication.ClientId
                $tokenSource.Cancel()
            }
            $tokenSource.Dispose()
        }

        ## Parse task results
        if ($taskAuthenticationResult.IsFaulted) {
            Write-Error -Exception $taskAuthenticationResult.Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureAuthenticationError' -TargetObject $AquireTokenParameters -ErrorAction Stop
        }
        if ($taskAuthenticationResult.IsCanceled) {
            Write-Error -Exception (New-Object System.Threading.Tasks.TaskCanceledException $taskAuthenticationResult) -Category ([System.Management.Automation.ErrorCategory]::OperationStopped) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureOperationStopped' -TargetObject $AquireTokenParameters -ErrorAction Stop
        }
        else {
            $AuthenticationResult = $taskAuthenticationResult.Result
        }
    }
    catch {
        Write-Error -Exception ($_.Exception) -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'GetMsalTokenFailureAuthenticationError' -TargetObject $AquireTokenParameters -ErrorAction Stop
    }

    Write-Verbose "[$((Get-Date).TimeofDay)] Ending $($myinvocation.mycommand)"
    # Return access token + Id token
    $AuthenticationResult
}
#EndRegion '.\Public\Get-EntraToken.ps1' 460
#Region '.\Public\Get-KVCertificateWithPrivateKey.ps1' 0
function Get-KVCertificateWithPrivateKey
{
    <#
      .SYNOPSIS
      This is a function to download certificate information from Azure KeyVault and export the private key as well.
 
      .DESCRIPTION
      This is a function to download certificate information from Azure KeyVault and export the private key as well.
 
      .EXAMPLE
      TODO: Write examples
 
      .PARAMETER KeyVaultCertificatePath
      The KeyVaultCertificatePath parameter is the path of the Keyvault certificate.
 
      .PARAMETER AccessToken
      The AccessToken parameter is the JWT you have to provide to do the action.
 
      .PARAMETER APIVersion
      The APIVersion parameter is the version of the Keyvault API.
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$KeyVaultCertificatePath, #https://ubuntukv415745.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb
        [Parameter(Mandatory)]
        [string]$AccessToken,
        [string]$APIVersion = '7.3'
    )

    # Force TLS 1.2.
    Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)"
    Write-Verbose "[$((Get-Date).TimeofDay)] Get-KVCertificateWithPrivateKey - Force TLS 1.2"
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    if ($AccessToken -notlike 'Bearer*')
    {
        $AccessToken = "Bearer $($AccessToken)"
    }

    $Headers = @{
        'Content-Type'  = 'application/json'
        'Authorization' = $AccessToken
    }

    $splat = @{
        'KeyVaultCertificatePath' = $KeyVaultCertificatePath
        'AccessToken'             = $AccessToken
        'APIVersion'              = $APIVersion
    }

    $CertInfo = Get-KVCertificateWithPublicKey @splat

    if ($null -eq $CertInfo.sid)
    {
        throw 'Unable to find private key information'
    }

    #Now we have certificate information let's find private key info
    Invoke-RestMethod -Uri "$($CertInfo.sid)?api-version=$($APIVersion)" -Headers $Headers
}
#EndRegion '.\Public\Get-KVCertificateWithPrivateKey.ps1' 63
#Region '.\Public\Get-KVCertificateWithPublicKey.ps1' 0
function Get-KVCertificateWithPublicKey
{
    <#
      .SYNOPSIS
      This is a function to download certificate information from Azure KeyVault.
 
      .DESCRIPTION
      This is a function to download certificate information from Azure KeyVault.
 
      .EXAMPLE
      TODO: Write examples
 
      .PARAMETER KeyVaultCertificatePath
      The KeyVaultCertificatePath parameter is the path of the Keyvault certificate.
 
      .PARAMETER AccessToken
      The AccessToken parameter is the JWT you have to provide to do the action.
 
      .PARAMETER APIVersion
      The APIVersion parameter is the version of the Keyvault API.
 
      #>

      [CmdletBinding()]
      param(
          [Parameter(Mandatory)]
          [string]$KeyVaultCertificatePath, #https://ubuntukv415745.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb
          [Parameter(Mandatory)]
          [string]$AccessToken,
          [string]$APIVersion = '7.3'
      )

      # Force TLS 1.2.
      Write-Verbose "[$((Get-Date).TimeofDay)] Starting $($myinvocation.mycommand)"
      Write-Verbose "[$((Get-Date).TimeofDay)] Get-KVCertificateWithPublicKey - Force TLS 1.2"
      [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

      if ($AccessToken -notlike 'Bearer*')
      {
          $AccessToken = "Bearer $($AccessToken)"
      }

      $Headers = @{
          'Content-Type'  = 'application/json'
          'Authorization' = $AccessToken
      }

      $CertURL = "$($KeyVaultCertificatePath)?api-version=$($APIVersion)"
      #$certURL = "https://ubuntukv415745.vault.azure.net/certificates/test/5d69153b75214245ab72fa21b9c06bfb?api-version=$APIVersion"

      Invoke-RestMethod -Uri $certURL -Headers $Headers
  }
#EndRegion '.\Public\Get-KVCertificateWithPublicKey.ps1' 52