MSAppProxy.ps1

# This file contains functions for Microsoft App Proxy

Add-Type -AssemblyName System.Web

# Registers App proxy agent to the Azure AD
# Apr 2nd 2020
# May 5th 2022: Added UpdateTrust
function Register-ProxyAgent
{
    <#
    .SYNOPSIS
    Registers a new MS App Proxy agent to Azure AD
 
    .DESCRIPTION
    Registers a new MS App Proxy agent to Azure AD. Currently Sync and PTA agents are supported.
 
    .Example
    $pt=Get-AADIntAccessTokenForPTA
    PS C:\>Register-AADIntProxyAgent -AccessToken $pt -MachineName server1.company.com -AgentType PTA -FileName server1-pta.pfx
 
    .Example
    $pt=Get-AADIntAccessTokenForPTA
    PS C:\>Register-AADIntProxyAgent -AccessToken $pt -MachineName server2.company.com -AgentType Sync -FileName server2-sync.pfx
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [String]$MachineName,
        [Parameter(Mandatory=$False)]
        [String]$FileName,
        [Parameter(Mandatory=$True)]
        [Validateset("PTA","Sync")]
        [String]$AgentType,
        [Parameter(Mandatory=$False)]
        $AgentGroup,
        [Parameter(ParameterSetName='update',Mandatory=$False)]
        [switch]$UpdateTrust,
        [Parameter(ParameterSetName='update',Mandatory=$False)]
        [String]$PfxFileName
        
    )
    Begin
    {
        $AgentInfo=@{
            "PTA"= @{
                    "FeatureString" = "PassthroughAuthentication"
                    "UserAgent" = "PassthroughAuthenticationConnector/1.5.643.0"

                }
            "Sync"= @{
                    "FeatureString" = "SyncFabric"
                    "UserAgent" = "SyncFabricConnector/1.1.96.0"

                }
            }
    }
    Process
    {
        if($UpdateTrust)
        {
            # Load the old certificate
            $cert = Load-Certificate -FileName $PfxFileName

            $tenantId = $cert.Subject.Split("=")[1]
        }
        else
        {
            # Get from cache if not provided
            $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"
            $tenantId = Get-TenantID -AccessToken $AccessToken
        }

        # Set some variables
        $OSLanguage="1033"
        $OSLocale="0409"
        $OSSku="8"
        $OSVersion="10.0.17763"
        
        # Create a private key and do something with it to get it stored
        $rsa=[System.Security.Cryptography.RSA]::Create(2048)
                
        # Initialize the Certificate Signing Request object
        $CN="" # The name doesn't matter
        $req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
        
        if($AgentType -eq "PTA")
        {
            # Key usage
            $req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new([System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::NonRepudiation -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment -bor [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DataEncipherment, $false))
            # TLS Web client authentication
            $oidCollection = [System.Security.Cryptography.OidCollection]::new()
            $oidCollection.Add([System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.2")) | Out-Null
            $req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($oidCollection, $true))


            # Add the public Key to the request
            $req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($req.PublicKey,$false))

            # Create the signing request
            $csr=$req.CreateSigningRequest()
            
        }
        elseif($AgentType -eq "Sync")
        {
            # This must be done this way cause MS CSR classes doesn't support attributes :(
            $csr = NewCSRforSync -MachineName $MachineName -PublicKey $req.PublicKey.EncodedKeyValue.RawData
        }

        $b64Csr=[convert]::ToBase64String($csr)

        # Create the request body
        if($UpdateTrust)
        {
            [xml]$config=Get-BootstrapConfiguration -Certificate $cert -MachineName $MachineName
            $body=@"
            <TrustRenewalRequest xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration.TrustRenewal" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <Base64Csr xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration">$b64Csr
</Base64Csr>
                <TrustRenewalRequestSettings>
                    <SystemSettingsInformation i:type="a:SystemSettings" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.RegistrationCommons" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
                        <a:MachineName>$machineName</a:MachineName>
                        <a:OsLanguage>$OSLanguage</a:OsLanguage>
                        <a:OsLocale>$OSLocale</a:OsLocale>
                        <a:OsSku>$OSSku</a:OsSku>
                        <a:OsVersion>$OSVersion</a:OsVersion>
                    </SystemSettingsInformation>
                    <ConnectorVersion>1.5.2482.0</ConnectorVersion>
                </TrustRenewalRequestSettings>
            </TrustRenewalRequest>
"@

            # Renew trust and get the certificate
            $response = Invoke-RestMethod -UseBasicParsing -Uri "$($config.BootstrapResponse.TrustRenewEndpoint)/RenewTrustCertificate" -Method Post -Body $body -Headers @{"Content-Type"="application/xml; charset=utf-8"} -Certificate $cert

            if($response.TrustRenewalResult.IsSuccessful.'#text' -eq "true")
            {
                # Get the certificate
                $b64Cert = $response.TrustRenewalResult.Certificate.'#text'
            }
            else
            {
                # Something went wrong
                Write-Error $response.TrustRenewalResult.ErrorMessage.'#text'
            }
        }
        else
        {
            $body=@"
            <RegistrationRequest xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Registration" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <Base64Csr>$b64Csr
</Base64Csr>
                <AuthenticationToken>$AccessToken</AuthenticationToken>
                <Base64Pkcs10Csr i:nil="true"/>
                <Feature>ApplicationProxy</Feature>
                <FeatureString>$($AgentInfo[$AgentType]["FeatureString"])</FeatureString>
                <RegistrationRequestSettings>
                    <SystemSettingsInformation i:type="a:SystemSettings" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.RegistrationCommons" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
                        <a:MachineName>$machineName</a:MachineName>
                        <a:OsLanguage>$OSLanguage</a:OsLanguage>
                        <a:OsLocale>$OSLocale</a:OsLocale>
                        <a:OsSku>$OSSku</a:OsSku>
                        <a:OsVersion>$OSVersion</a:OsVersion>
                    </SystemSettingsInformation>
                    <PSModuleVersion>1.5.643.0</PSModuleVersion>
                    <SystemSettings i:type="a:SystemSettings" xmlns:a="http://schemas.datacontract.org/2004/07/Microsoft.ApplicationProxy.Common.Utilities.SystemSettings">
                        <a:MachineName>$machineName</a:MachineName>
                        <a:OsLanguage>$OSLanguage</a:OsLanguage>
                        <a:OsLocale>$OSLocale</a:OsLocale>
                        <a:OsSku>$OSSku</a:OsSku>
                        <a:OsVersion>$OSVersion</a:OsVersion>
                    </SystemSettings>
                </RegistrationRequestSettings>
                <TenantId>$tenantId</TenantId>
                <UserAgent>$($AgentInfo[$AgentType]["UserAgent"])</UserAgent>
            </RegistrationRequest>
"@

            # Register the app and get the certificate
            $response = Invoke-RestMethod -UseBasicParsing -Uri "https://$tenantId.registration.msappproxy.net/register/RegisterConnector" -Method Post -Body $body -Headers @{"Content-Type"="application/xml; charset=utf-8"}

            if($response.RegistrationResult.IsSuccessful -eq "true")
            {
                # Get the certificate
                $b64Cert = $response.RegistrationResult.Certificate
            }
            else
            {
                # Something went wrong
                Write-Error $response.RegistrationResult.ErrorMessage
            }
        }
        
        if(![string]::IsNullOrEmpty($b64Cert))
        {
        
            # Convert certificate to byte array
            $binCert = [convert]::FromBase64String($b64Cert)
            
            # Create a new x509certificate
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -band [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

            # Get the instance Id (=Agent Id)
            foreach($extension in $cert.Extensions)
            {
                if($extension.Oid.Value -eq "1.3.6.1.4.1.311.82.1")
                {
                    $InstanceID = [guid]$extension.RawData
                }
            }

            if([string]::IsNullOrEmpty($FileName))
            {
                $FileName = "$($MachineName)_$($tenantId)_$($InstanceID).pfx"
            }

            # Store the private key so that it can be exported
            $cspParameters = [System.Security.Cryptography.CspParameters]::new()
            $cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
            $cspParameters.ProviderType = 24
            $cspParameters.KeyContainerName ="AADInternals"
            
            # Set the private key
            $privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters)
            $privateKey.ImportParameters($rsa.ExportParameters($true))
            $cert.PrivateKey = $privateKey

            # Export the certificate to pfx
            $binCert = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
            $binCert | Set-Content $fileName -Encoding Byte

            # Remove the private key from the store
            $privateKey.PersistKeyInCsp=$false
            $privateKey.Clear()

            

            if($UpdateTrust)
            {
                Write-Host "$AgentType Agent ($InstanceID) registered as $MachineName"
            }
            else
            {
                Write-Host "$AgentType Agent ($InstanceID) certificate renewed for $MachineName"
            }
            Write-Host "Certificate saved to $FileName"

            # We need to register the agent to a group
            if($AgentType -eq "Sync" -and [string]::IsNullOrEmpty($AgentGroup) -ne $true)
            {
                Add-ProxyAgentToGroup -AccessToken $AccessToken -Agent $InstanceID -Group $AgentGroup
            }
        }
        

        
    }
}

# Gets list of publishing agents
# Apr 3rd 2020
function Get-ProxyAgents
{
    <#
    .SYNOPSIS
    Shows the list of MS App Proxy agents
 
    .DESCRIPTION
    Shows the list of MS App Proxy authentication and provisioning agents
 
    .Example
    Get-AADIntProxyAgents | ft
 
    id machineName externalIp status supportedPublishingTypes
    -- ----------- ---------- ------ ------------------------
    51f3afd9-685b-413a-aafa-bab0d556ea4b this.is.a.fake 67.35.155.73 active {authentication}
    51a061a0-968d-48b8-951e-5ae9d9a0441f server1.company.com 93.188.31.116 inactive {authentication}
    49c9ad46-c067-42f6-a678-dfd938c27789 server2.company.com 102.20.104.213 inactive {provisioning}
 
    .Example
    $pt=Get-AADIntAccessTokenForPTA
 
    PS C:\>Get-AADIntProxyAgents -AccessToken $pt | pt
 
    id machineName externalIp status supportedPublishingTypes
    -- ----------- ---------- ------ ------------------------
    51f3afd9-685b-413a-aafa-bab0d556ea4b this.is.a.fake 67.35.155.73 active {authentication}
    51a061a0-968d-48b8-951e-5ae9d9a0441f server1.company.com 93.188.31.116 inactive {authentication}
    49c9ad46-c067-42f6-a678-dfd938c27789 server2.company.com 102.20.104.213 inactive {provisioning}
    
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken
    )
    Begin
    {
        $publishingTypes = @(     # Roles that can access the agent
            #"appProxy" # ApplicationAdmin, GlobalAdmin
            "authentication"      # GlobalAdmin
            "provisioning"        # GlobalAdmin
            "exchangeOnline"      # GlobalAdmin
            #"intunePfx" # GlobalAdmin
            #"oflineDomainJoin" # GlobalAdmin
            "adAdministration"    # DirSyncAdmin, GlobalAdmin
            #"unknownFutureValue" #
        )
    }
    Process
    {
        # Get from cache if not provided
        $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"

        # Get the tenant id and instance id from the certificate
        $TenantId = Get-TenantID -AccessToken $AccessToken
        
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-gateway-serviceRoot" =""
        }

        
        foreach($type in $publishingTypes)
        {
            $agents = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('$type')/agents" -Method Get -Headers $headers -ErrorAction SilentlyContinue

            # Return
            if($agents)
            {        
                $agents.value
            }
        }

    }
}

# Gets list of agent groups
# Apr 6th 2020
function Get-ProxyAgentGroups
{
    <#
    .SYNOPSIS
    Lists MS App Proxy agent groups
 
    .DESCRIPTION
    Lists MS App Proxy agent groups
 
    .Example
    Get-AADIntAgentProxyGroups
 
    TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
    ConfigurationDisplayName : company.com
    ConfigurationResourceName : company.com
    ConfigurationPublishingType : provisioning
    id : 4b6ffe82-bfe2-4357-814c-09da95399da7
    displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
    publishingType : provisioning
    isDefault : False
 
    .Example
    $pt=Get-AADIntAccessTokenForPTA
 
    PS C:\>Get-AADIntProxyGroups -AccessToken $pt
 
    TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
    ConfigurationDisplayName : company.com
    ConfigurationResourceName : company.com
    ConfigurationPublishingType : provisioning
    id : 4b6ffe82-bfe2-4357-814c-09da95399da7
    displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
    publishingType : provisioning
    isDefault : False
    
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$AccessToken
    )
    Process
    {
        # Get from cache if not provided
        $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"

        # Get the tenant id and instance id from the certificate
        $TenantId = Get-TenantID -AccessToken $AccessToken
        
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-gateway-serviceRoot" =""
        }

        $response = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agentGroups?`$expand=agents" -Method Get -Headers $headers 
        
        # return
        $response.value
    }
}

# Gets the list of proxy configurations
#function Get-ProxyConfigurations

# Creates a new proxy agent group
# Apr 6th 2020
function New-ProxyAgentGroup
{
    <#
    .SYNOPSIS
    Creates an MS App Proxy agent group
 
    .DESCRIPTION
    Creates an MS App Proxy agent group
 
    .Example
    Get-AADIntAgentProxyGroups
 
    TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
    ConfigurationDisplayName : company.com
    ConfigurationResourceName : company.com
    ConfigurationPublishingType : provisioning
    id : 4b6ffe82-bfe2-4357-814c-09da95399da7
    displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
    publishingType : provisioning
    isDefault : False
 
    .Example
    $pt=Get-AADIntAccessTokenForPTA
 
    PS C:\>Get-AADIntProxyGroups -AccessToken $pt
 
    TenantId : ea664074-37dd-4797-a676-b0cf6fdafcd4
    ConfigurationDisplayName : company.com
    ConfigurationResourceName : company.com
    ConfigurationPublishingType : provisioning
    id : 4b6ffe82-bfe2-4357-814c-09da95399da7
    displayName : Group-company.com-42660f4a-9e66-4a08-ac17-2a2e0d8b993e
    publishingType : provisioning
    isDefault : False
    
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [String]$DisplayName,
        [Parameter(Mandatory=$True)]
        [String]$ConfigurationDisplayName,
        [Parameter(Mandatory=$True)]
        [String]$ConfigurationResourceName
    )
    Process
    {
        # Get from cache if not provided
        $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"

        # Get the tenant id and instance id from the certificate
        $TenantId = Get-TenantID -AccessToken $AccessToken
        
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-gateway-serviceRoot" =""
            "Content-Type" = "application/json"
        }

        # First, create the agent group with the given name
        $Body = "{""displayName"":""$DisplayName""}"
        $response  = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agentGroups" -Method POST -Headers $headers -Body $Body

        $Body = "{""displayName"":""$ConfigurationDisplayName"",""resourceName"":""$ConfigurationResourceName"",""agentGroups"":[{""id"":""$($response.id)""}]}"
        $response2 = Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/publishedResources" -Method POST -Headers $headers -Body $Body
        
        # Extract the information and create the return value
        $attributes=[ordered]@{}

        $attributes["id"]=$response.id
        $attributes["displayName"]=$response.displayName
        $attributes["publishingType"]=$response.publishingType
        $attributes["isDefault"]=$response.isDefault
        
        $attributes["ConfigurationId"]=$response2.id
        $attributes["ConfigurationDisplayName"]=$response2.displayName
        $attributes["ConfigurationResourceName"]=$response2.resourceName
        $attributes["ConfigurationPublishingType"]=$response2.publishingType
        
        # return
        New-Object PSObject -Property $attributes
    }
}

# Adds the given agent to given group
# Apr 7th 2020
function Add-ProxyAgentToGroup
{

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [guid]$Agent,
        [Parameter(Mandatory=$True)]
        [guid]$Group
    )
    
    Process
    {
        # Get from cache if not provided
        $AccessToken = Get-AccessTokenFromCache -AccessToken $AccessToken -Resource "https://proxy.cloudwebappproxy.net/registerapp" -ClientId "cb1056e2-e479-49de-ae31-7812af012ed8"

        # Get the tenant id and instance id from the certificate
        $TenantId = Get-TenantID -AccessToken $AccessToken
       
        $body="{""@odata.id"":""https://$TenantId.admin.msappproxy.net:443/onPremisesPublishingProfiles('provisioning')/agentGroups('$($Group.toString())')""}"

        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "x-ms-gateway-serviceRoot" =""
            "Content-Type" = "application/json"
        }

        Invoke-RestMethod -UseBasicParsing -Uri "https://$TenantId.admin.msappproxy.net/onPremisesPublishingProfiles('provisioning')/agents('$($Agent.toString())')/agentGroups/`$ref" -Method Post -Headers $headers -Body $body

        Write-Host "Agent ($($Agent.toString())) added to group ($($Group.toString()))"
    }
}