src/cmdlets/Remove-GraphApplicationCertificate.ps1

# Copyright 2021, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. (import-script ../graphservice/ApplicationAPI)
. (import-script common/CommandContext)

<#
.SYNOPSIS
Removes the certificate configuration from an Entra ID application that allows the application to be authenticated using specific certificates.
 
.DESCRIPTION
Certificate credential configuration on the application object allows running application code to obtain access tokens with the application's identity. Remove-GraphApplicationCertificate removes one or more certificates from the configuration; once removed, a private key for that certificate will no longer be a valid credential for authentication.
 
Note that the command only affects the application and has no effect on any state stored in a local or remote certificate store.
 
The application to be modified may be specified using either the application identifier through the AppId parameter or the Entra ID object identifier through the AppObjectId parameter.
 
The certificate to remove may be specified using the KeyId parameter or the Thumbprint parameter, and there is also an option to remove all certificates using the AllCertificates parameter.
 
.PARAMETER AppId
The Entra ID application identifier of the application for which to remove certificates.
 
.PARAMETER AppObjectId
The object identifier of the application for which to remove certificates.
 
.PARAMETER KeyId
The unique identifier for the certificate for the certificate to remove from this application. The identifier is generated by Entra ID at the time that the certificate is configured for the application.
 
.PARAMETER Thumbprint
The certificate thumbprint of the certificate to remove.
 
.PARAMETER AllCertificates
Specify AllCertificates to remove all certificates from the application
 
.PARAMETER Connection
Specify the Connection parameter to use as an alternative connection to the current connection.
 
.OUTPUTS
The Remove-GraphApplicationCertificate command has no output.
 
.EXAMPLE
Remove-GraphApplicationCertificate a5ebc719-fee5-4eb8-963c-4f1cf24ae813 -AllCertificates
 
This example uses the AllCertificates option to remove all configured certificates for the application with identifier a5ebc719-fee5-4eb8-963c-4f1cf24ae813
 
.EXAMPLE
Remove-GraphApplicationCertificate -AppId a5ebc719-fee5-4eb8-963c-4f1cf24ae813 -Thumbprint C528DE4D806C4CA374FF5C1849B3586278AF6193
 
In this example, the application identifier of the application as well as the thumbprint of the certificte to remove are specified to Remove-GraphApplicationCertificate.
 
.EXAMPLE
Get-GraphApplication -Name 'Backup application' | Remove-GraphApplicationCertificate -AllCertificates
 
This example shows how the output of Get-GraphApplication may be piped to Remove-GraphApplicationCertificate. Here the Get-GraphApplication command is used to find the target application by name rather than application identifier, and the result is piped to Remove-GraphApplicationCertificate so that the certificates from that application will all be removed.
 
.EXAMPLE
Get-GraphApplicationCertificate -AppId 9a89159c-2abb-4faf-9435-269657931e1e |
    Where-Object NotAfter -lt ([DateTime]::now) |
    Remove-GraphApplicationCertificate
 
This example demonstrates one way to remove expired certificates. The Get-GraphApplicationCertificate command is used to retrieve the certificates from an application, and the results filtered through Where-Object on the NotAfter property. Only certificates with a NotAfter time less than the current time, i.e. those where the NotAfter time is already in the past, will be returned. These expired certificates are then piped to Remove-GraphApplicationCertificate which removes them from the application.
 
.EXAMPLE
Get-GraphApplication -All | Get-GraphApplicationCertificate |
    Where-Object NotAfter -lt ([DateTime]::now) |
    Remove-GraphApplicationCertificate
 
This example shows how to remove expired certificates for all the applications in the organization. The Get-GraphApplication command is used with the -All parameter to enumerate all applications in the organization. The result is piped to Get-GraphApplicationCertificate to retrieve the certificates for all those applications. That result is piped to Where-Object which filters the expiration date of the certificate exposed through the NotAfter property of the returned certificate object to filter the result only to those certificates that have expired. Finally, that filtered result is piped to Remove-GraphApplicationCertificate to remove those expired certificates.
 
.LINK
Set-GraphApplicationCertificate
Get-GraphApplicationCertificate
New-GraphApplication
Find-GraphApplicationLocalCertificate
Connect-GraphApi
#>

function Remove-GraphApplicationCertificate {
    [cmdletbinding(supportsshouldprocess=$true, confirmimpact='High', positionalbinding=$false)]
    param(
        [parameter(parametersetname='AppIdFromUniqueId', position=0, mandatory=$true)]
        [parameter(parametersetname='AppIdFromThumbprint', position=0, mandatory=$true)]
        [parameter(parametersetname='AppIdAllCertificates', position=0, mandatory=$true)]
        [Guid] $AppId,

        [parameter(parametersetname='ObjectIdFromUniqueId', valuefrompipelinebypropertyname=$true, mandatory=$true)]
        [parameter(parametersetname='ObjectIdFromThumbprint', valuefrompipelinebypropertyname=$true, mandatory=$true)]
        [parameter(parametersetname='ObjectIdAllCertificates', valuefrompipelinebypropertyname=$true, mandatory=$true)]
        [Alias('Id')]
        [Guid] $AppObjectId,

        [parameter(parametersetname='AppIdFromUniqueId', valuefrompipelinebypropertyname=$true, mandatory=$true)]
        [parameter(parametersetname='ObjectIdFromUniqueId', valuefrompipelinebypropertyname=$true, mandatory=$true)]
        [Guid] $KeyId,

        [parameter(parametersetname='AppIdFromThumbprint', position=1, mandatory=$true)]
        [parameter(parametersetname='ObjectIdFromThumbprint', position=1, mandatory=$true)]
        $Thumbprint = $null,

        [parameter(parametersetname='AppIdAllCertificates', mandatory=$true)]
        [parameter(parametersetname='ObjectIdAllCertificates', mandatory=$true)]
        [switch] $AllCertificates,

        [PSCustomObject] $Connection = $null
    )

    begin {
        $commandContext = new-so CommandContext $connection $null $null $null $::.ApplicationAPI.DefaultApplicationApiVersion
        $appAPI = new-so ApplicationAPI $commandContext.connection $commandContext.version
        $appToCredentials = @{}
    }

    process {
        Enable-ScriptClassVerbosePreference

        $remainingCredentials = $null

        $targetObjectId = if ( $AppObjectId ) {
            $AppObjectId
        } elseif ($AppId ) {
            $appAPI |=> GetApplicationByAppId $AppId | select -expandproperty id
        } else {
            Write-Error "Unexpected argument -- an app id or object id must be specified"
        }

        $keyClientFilter = if ( $AllCertificates.IsPresent ) {
            { $true }
        } elseif ( $KeyId ) {
            { $_.KeyId -eq $KeyId }
        } elseif ( $Thumbprint ) {
            { $_.CustomKeyIdentifier -eq $Thumbprint }
        } else {
            Write-Error "An AppId with Thumbprint or KeyId was not specified or AllCertificates was not specified"
        }

        # This returns ALL credentials, not just certificates. If it didn't, the naive API used to add certificates
        # (or just replace only the certs and leave other credential types alone) would remove anything that wasn't
        # a certificate.
        $keyCredentials = $::.ApplicationHelper |=> QueryApplications $null $targetObjectId $null $null $null $commandContext.version $null null $commandContext.connection keyCredentials |
          select -expandproperty keyCredentials

        $certToRemove = if ( ! $keyCredentials -and ! ($keyCredentials | gm id -erroraction ignore ) ) {
            Write-Error "No certificates could be found for application with object identifier '$targetObjectId'"
        } else {
            # Limit the removal to only the certificates with a filter on credential type
            $keyCredentials | where $keyClientFilter | where type -eq 'AsymmetricX509Cert'
        }

        if ( ! $certToRemove ) {
            Write-Error "The specified certificate could not be found for the application with object identifier '$targetObjectId'"
        }

        # Excise the certs to be removed from the set of all credentials -- this leaves
        # all the non-certificates and any certificates not targeted by this command
        $remainingCredentials = $keyCredentials | where KeyId -notin $certToRemove.keyId

        # Now group all the apps together with a dictionary to avoid duplicates.
        # Within each app's dictionary entry, include all the remaining credentials
        # within a nested hash table that again prevents duplicates. Duplicates can happen
        # when the same application is specified in the pipeline with a different cert, quite
        # possibly due to a certificate object being supplied to the command with the parameters
        # for the application's object id and key id being bound as parameters for instance.

        if ( $remainingCredentials -eq $null ) {
            $remainingCredentials = @()
        }

        $newApp = $false
        $currentAppCredentials = $appToCredentials[$targetObjectId]

        if ( ! $currentAppCredentials ) {
            $newApp = $true
            $currentAppCredentials = @{
                AppObjectId = $targetObjectId
                RemainingCredentials = @{}
            }
        }

        foreach ( $remainingCert in $remainingCredentials ) {
            $currentAppCredentials.RemainingCredentials[$remainingCert.KeyId] = $remainingCert
        }

        if ( $newApp ) {
            $appToCredentials[$targetObjectId] = $currentAppCredentials
        }
    }

    end {
        # Now for each app, update the credentials according to the remaining credentials
        foreach ( $appCredentials in $appToCredentials.Values ) {
            $appAPI |=> SetKeyCredentials $appCredentials.AppObjectId $appCredentials.RemainingCredentials.Values
        }
    }
}