functions/azure/aad/Assert-AzureAdApp.ps1

# <copyright file="Assert-AzureAdApp.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

<#
.SYNOPSIS
Ensures that an AzureAD application with the specified configuration exists.

.DESCRIPTION
Ensures that an AzureAD application with the specified configuration exists, creating or updating as necessary.

.PARAMETER DisplayName
Used to search for an existing AzureAD application or create one with the specified name.

.PARAMETER IdentifierUri
The URL to the application homepage. Can only be updated for an existing AzureAD application when also updating the 'ReplyUrls' property.

.PARAMETER ReplyUrls
The application reply urls. Can be updated for an existing AzureAD application.

.PARAMETER EnableAccessTokenIssuance
When true, allows the AzureAD application to issue access tokens (used for implicit flow). Can only be updated for an existing AzureAD application when also updating the 'ReplyUrls' property.

.PARAMETER EnableIdTokenIssuance
When true, allows the AzureAD application to issue ID tokens (used for implicit & hybrid flows). Can only be updated for an existing AzureAD application when also updating the 'ReplyUrls' property.

.OUTPUTS
Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphApplication
#>

function Assert-AzureAdApp
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [string] $DisplayName,

        [Parameter()]
        [Alias("AppUri")]
        [string] $IdentifierUri,
        
        [Parameter()]
        [string[]] $ReplyUrls,

        [Parameter()]
        [switch] $EnableAccessTokenIssuance,

        [Parameter()]
        [switch] $EnableIdTokenIssuance
    )

    # Helper functions
    function _buildAppWebConfig($BoundParameters)
    {
        # Construct an object that represents the 'webApplication' type
        # ref: https://learn.microsoft.com/en-us/graph/api/resources/webapplication?view=graph-rest-1.0
        $appWebConfig = @{
            implicitGrantSettings = @{
                enableAccessTokenIssuance = $EnableAccessTokenIssuance.ToBool()
                enableIdTokenIssuance = $EnableIdTokenIssuance.ToBool()
            }
        }
        if ($BoundParameters.ContainsKey("ReplyUrls")) {
            # Use the ReplyUrls that have been specified on the call to Assert-AzureAdApp
            $appWebConfig += @{ redirectUris = $ReplyUrls }
        }
        elseif ($app) {
            # ReplyUrls not specified, use the app's existing values
            $appWebConfig += @{ redirectUris = $app.Web.redirectUri }
        }
        else {
            # ReplyUrls not specified and app does not yet exist
            $appWebConfig += @{ redirectUris = @() }
        }
        return $appWebConfig
    }

    #
    # Main implementation
    #

    # Check whether we have a valid AzPowerShell connection, but no subscription-level access is required
    _EnsureAzureConnection -AzPowerShell -TenantOnly -ErrorAction Stop | Out-Null
    
    Write-Host "Ensuring Azure AD application {$DisplayName} exists..."

    $app = Get-AzADApplication -DisplayNameStartWith $DisplayName | `
                Where-Object {$_.DisplayName -eq $DisplayName}
    
    if ($app) {
        Write-Host "Found existing app [AppId=$($app.AppId)] [ObjectId=$($app.Id)]"
        $appNeedsUpdating = $false
        $ReplyUrlsOk = $true
        ForEach ($ReplyUrl in $ReplyUrls) {
            if (!$app.Web.RedirectUri -or !$app.Web.RedirectUri.Contains($ReplyUrl)) {
                $ReplyUrlsOk = $false
                Write-Host "Reply URL $ReplyUrl not present in app"
            }
        }

        # Check whether 'web' settings need updating
        if (
            !$ReplyUrlsOk -or 
            ($app.Web.ImplicitGrantSetting.EnableAccessTokenIssuance -ne $EnableAccessTokenIssuance) -or 
            ($app.Web.ImplicitGrantSetting.EnableIdTokenIssuance -ne $EnableIdTokenIssuance)
        ) {
            $additionalUpdateParams = @{
                objectId = $app.Id
                web = _buildAppWebConfig($PSBoundParameters)
            }
            $appNeedsUpdating = $true
        }

        # Check whether other optional settings need to be updated
        if ($PSBoundParameters.ContainsKey("IdentifierUri") -and $app.IdentifierUri -ne $IdentifierUri) {
            $appNeedsUpdating = $true
        }

        if ($appNeedsUpdating) {
            Write-Host "Updating app"
            # Remove parameters that cannot be 'splatted' into Update-AzADApplication
            $PSBoundParameters.Remove("ReplyUrls") | Out-Null
            $PSBoundParameters.Remove("EnableAccessTokenIssuance") | Out-Null
            $PSBoundParameters.Remove("EnableIdTokenIssuance") | Out-Null

            Write-Verbose "PSBoundParameters:`n$($PSBoundParameters | ConvertTo-Json -Depth 100)"
            Write-Verbose "additionalUpdateParams:`n$($additionalUpdateParams | ConvertTo-Json -Depth 100)"
            Update-AzADApplication @PSBoundParameters @additionalUpdateParams | Out-Null
            $app = Get-AzADApplication -ObjectId $app.Id
        }
    } else {
        Write-Host "Creating new app"
        $additionalCreateParams = @{}
        if ($ReplyUrls.Count -gt 0) {
            $additionalCreateParams += @{
                web = _buildAppWebConfig($PSBoundParameters)
            }
        }
        # Remove parameters that cannot be 'splatted' into New-AzADApplication
        $PSBoundParameters.Remove("ReplyUrls") | Out-Null
        $PSBoundParameters.Remove("EnableAccessTokenIssuance") | Out-Null
        $PSBoundParameters.Remove("EnableIdTokenIssuance") | Out-Null
        Write-Verbose "PSBoundParameters:`n$($PSBoundParameters | ConvertTo-Json -Depth 100)"
        Write-Verbose "additionalCreateParams:`n$($createParams | ConvertTo-Json -Depth 100)"

        $app = New-AzADApplication @PSBoundParameters @additionalCreateParams
        Write-Host "Created new app with AppId $($app.AppId) [IdentifierUri=$($app.IdentifierUri)]"
    }

    return $app
}