Functions/Register-MicrosoftOnlineAutomation.ps1

<#
    .SYNOPSIS
        Register an application with key credentials and permissions for
        PowerShell automation in Azure AD.
 
    .DESCRIPTION
        This command will register an application in the Azure AD used set
        perform PowerShell automation tasks. It will also create a local
        certificate and add it as authentication key to the application.
        Finally the permissions in the tenant is applied to the service
        principal
 
    .INPUTS
        None.
 
    .OUTPUTS
        MicrosoftOnlineFever.Tenant. Tenant for the created application.
 
    .EXAMPLE
        PS C:\> Register-MicrosoftOnlineAutomation
        Create a new application in the Azure AD for the automation with
        default properties.
 
    .LINK
        https://github.com/claudiospizzi/MicrosoftOnlineFever
#>

function Register-MicrosoftOnlineAutomation
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Scope='Function', Target='Register-MicrosoftOnlineAutomation')]
    param
    (
        # Tenant name used to store this regsitered app in the module context.
        [Parameter(Mandatory = $false)]
        [System.String]
        $Name,

        # Credential to connect to the Azure AD. Only usable if two factor is
        # not enabled. If the credential is not specified, a UI popup will
        # prompt for the Microsoft Online login.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        $Credential,

        # Name of the Azure AD application.
        [Parameter(Mandatory = $false)]
        [System.String]
        $ApplicationName = 'PowerShell Automation',

        # Identifier Uri for the Azure AD application. The placeholder
        # <TenantDomain> is replaced with the actual tenant domain
        [Parameter(Mandatory = $false)]
        [System.String]
        $ApplicationIdentifierUri = 'https://<TenantDomain>/powershell-automation',

        # Directory role used by the Azure AD application.
        [Parameter(Mandatory = $false)]
        [System.String[]]
        $ApplicationDirectoryRole = @('Global Administrator', 'Exchange Administrator', 'SharePoint Administrator'),

        # API permissions for the Azure AD application.
        [Parameter(Mandatory = $false)]
        [System.String[]]
        $ApplicationApiPermission = @('Office 365 Exchange Online:Exchange.ManageAsApp', 'Office 365 SharePoint Online:Sites.FullControl.All'),

        # Fallback user if the appication based login is not possible.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        $FallbackCredential,

        # Permission for the fallback user.
        [Parameter(Mandatory = $false)]
        [System.String[]]
        $FallbackDirectoryRole = @('Global Reader', 'License Administrator', 'User Administrator'),

        # Use the preview modules if available.
        [Parameter(Mandatory = $false)]
        [Alias('Preview')]
        [Switch]
        $UsePreviewModule
    )

    Test-MicrosoftOnlineModuleDependency -Scope 'AzureAD' -UsePreviewModule:$UsePreviewModule


    ## Azure AD Connection

    $context = [PSCustomObject] @{ TenantId = '5f36d76e-9089-4ef3-94fc-d1758088e39a'; TenantDomain = 'arcadespizzilab.onmicrosoft.com' }

    if ($PSBoundParameters.ContainsKey('Credential'))
    {
        Write-Verbose 'Azure AD Connection => Open by using the specified credential'

        $context = Connect-AzureAD -Credential $Credential
    }
    else
    {
        Write-Verbose 'Azure AD Connection => Open by using the UI to login...'

        $context = Connect-AzureAD
    }

    # Exit if the connection was not succesful, e.g. is empty.
    if ($null -eq $context)
    {
        throw 'User authentication not successful'
    }

    # Patch the application identifier uri with the tenant domain.
    $ApplicationIdentifierUri = $ApplicationIdentifierUri.Replace('<TenantDomain>', $context.TenantDomain)

    Write-Verbose "Azure AD Connection => TenantId: $($context.TenantId)"
    Write-Verbose "Azure AD Connection => TenantDomain: $($context.TenantDomain)"


    ## Azure AD Application

    $application = Get-AzureADApplication | Where-Object { $_.IdentifierUris -contains $ApplicationIdentifierUri }

    if ($null -ne $application)
    {
        if ($application.DisplayName -ne $ApplicationName)
        {
            throw "Mismatch between existing application '$($application.DisplayName)' and the desired application '$ApplicationName'"
        }

        Write-Verbose "Azure AD Application => Use existing Application"
    }
    else
    {
        # Exit if the user does not confirm the application registration.
        if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Register Application')) { return }

        Write-Verbose "Azure AD Application => Register the Application"

        $applicationSplat = @{
            DisplayName    = $ApplicationName
            IdentifierUris = $ApplicationIdentifierUri
        }
        $application = New-AzureADApplication @applicationSplat
    }

    $applicationReplyUrl = 'https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/{0}/isMSAApp/' -f $application.AppId
    if ($application.ReplyUrls -notcontains $applicationReplyUrl)
    {
        Write-Verbose "Azure AD Application => Register the Reply Url"

        Set-AzureADApplication -ObjectId $application.ObjectId -ReplyUrls $applicationReplyUrl
        $application = Get-AzureADApplication | Where-Object { $_.IdentifierUris -contains $ApplicationIdentifierUri }
    }

    Write-Verbose "Azure AD Application => AppId: $($application.AppId)"
    Write-Verbose "Azure AD Application => ObjectId: $($application.ObjectId)"
    Write-Verbose "Azure AD Application => DisplayName: $($application.DisplayName)"


    ## Azure AD Application Cert

    $certificateStore   = 'Cert:\CurrentUser\My'
    $certificateSubject = 'CN={0}, OU={1}, O={2}' -f $context.TenantDomain, $application.AppId, $context.TenantId

    $certificate = Get-ChildItem -Path $certificateStore | Where-Object { $_.Subject -eq $certificateSubject -and $_.HasPrivateKey } | Select-Object -First 1

    if ($null -ne $certificate)
    {
        Write-Verbose "Azure AD Application Cert => Use existing Certificate"
    }
    else
    {
        # Exit if the user does not confirm the application registration.
        if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Create Certificate')) { return }

        Write-Verbose "Azure AD Application Cert => Generate the Certificate"

        $certificateSplat = @{
            Subject           = $certificateSubject
            NotAfter          = [System.DateTime]::Now.AddYears(10)
            CertStoreLocation = $certificateStore
            KeyExportPolicy   = 'Exportable'
            Provider          = 'Microsoft Enhanced RSA and AES Cryptographic Provider'
            Type              = 'CodeSigningCert'
            KeySpec           = 'Signature'
        }
        $certificate = New-SelfSignedCertificate @certificateSplat
    }

    Write-Verbose "Azure AD Application Cert => Thumbprint: $($certificate.Thumbprint)"
    Write-Verbose "Azure AD Application Cert => Subject: $($certificate.Subject)"


    ## Azure AD Application Cert Key

    $certKeyIdentifier = $certificate.Thumbprint.Substring(0, 30)

    $certKey = Get-AzureADApplicationKeyCredential -ObjectId $application.ObjectId | Where-Object { [System.Text.Encoding]::Default.GetString($_.CustomKeyIdentifier) -eq $certKeyIdentifier }

    if ($null -ne $certKey)
    {
        Write-Verbose "Azure AD Application Cert Key => Use existing Key"
    }
    else
    {
        # Exit if the user does not confirm the application registration.
        if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Create App Cert Key')) { return }

        Write-Verbose "Azure AD Application Cert Key => Register the Key"

        $certKeySplat = @{
            ObjectId            = $application.ObjectId
            CustomKeyIdentifier = $certKeyIdentifier
            Type                = 'AsymmetricX509Cert'
            Usage               = 'Verify'
            Value               = [System.Convert]::ToBase64String($certificate.GetRawCertData())
            EndDate             = $certificate.NotAfter
        }
        $certKey = New-AzureADApplicationKeyCredential @certKeySplat
    }

    Write-Verbose "Azure AD Application Cert Key => KeyId: $($certKey.KeyId)"


    ## Azure AD Application Secret Key

    $secretKeyIdentifier = [System.Guid]::NewGuid().Guid.Replace('-', '').Substring(0, 30)

    Write-Verbose "Azure AD Application Secret Key => Register the key"

    # Exit if the user does not confirm the application registration.
    if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Create App Secret Key')) { return }

    $secretKey = New-AzureADApplicationPasswordCredential -ObjectId $application.ObjectId -CustomKeyIdentifier $secretKeyIdentifier -EndDate ([System.DateTime]::UtcNow.AddYears(10))


    ## Azure AD Application Principal

    $principal = Get-AzureADServicePrincipal -All $true | Where-Object { $_.AppId -eq $application.AppId }

    if ($null -ne $principal)
    {
        Write-Verbose "Azure AD Application Principal => Use existing Principal"
    }
    else
    {
        # Exit if the user does not confirm the application registration.
        if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Create App Principal')) { return }

        Write-Verbose "Azure AD Application Principal => Create the Principal"

        $principal = New-AzureADServicePrincipal -AppId $application.AppId
    }

    Write-Verbose "Azure AD Application Principal => ObjectId: $($principal.ObjectId)"


    ## Azure AD Application Role Template

    for ($i = 0; $i -lt $ApplicationDirectoryRole.Count; $i++)
    {
        $role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq $ApplicationDirectoryRole[$i] }

        if ($null -eq $role)
        {
            $roleTemplate = Get-AzureADDirectoryRoleTemplate | Where-Object { $_.DisplayName -eq $ApplicationDirectoryRole[$i] }

            if ($null -ne $roleTemplate)
            {
                # Exit if the user does not confirm the application registration.
                if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Enable Role Template')) { return }

                Write-Verbose "Azure AD Application Role Template [$i] => Enable Role Template"

                Enable-AzureADDirectoryRole -RoleTemplateId $roleTemplate.ObjectId
            }
            else
            {
                Write-Verbose "Azure AD Application Role Template [$i] => Role Template missing"
            }
        }
        else
        {
            Write-Verbose "Azure AD Application Role Template [$i] => Use existing Role Template"
        }

        Write-Verbose "Azure AD Application Role Template [$i] => DisplayName: $($ApplicationDirectoryRole[$i])"
    }


    ## Azure AD Application Role

    for ($i = 0; $i -lt $ApplicationDirectoryRole.Count; $i++)
    {
        $role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq $ApplicationDirectoryRole[$i] }

        if ($null -ne $role)
        {
            $member = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId | Where-Object { $_.ObjectType -eq 'ServicePrincipal' -and $_.AppId -eq $application.AppId }

            if ($null -ne $member)
            {
                Write-Verbose "Azure AD Application Role [$i] => Use existing Role"
            }
            else
            {
                # Exit if the user does not confirm the application registration.
                if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Add App to Role')) { return }

                Write-Verbose "Azure AD Application Role [$i] => Add App to Role"

                $member = Add-AzureADDirectoryRoleMember -ObjectId $role.ObjectId -RefObjectId $principal.ObjectId
            }

            Write-Verbose "Azure AD Application Role [$i] => DisplayName: $($ApplicationDirectoryRole[$i])"
        }
    }


    ## Azure AD API Permissions

    for ($i = 0; $i -lt $ApplicationApiPermission.Count; $i++)
    {
        $apiServicePrincipalName = $ApplicationApiPermission[$i].Split(':')[0]
        $apiPermissionName       = $ApplicationApiPermission[$i].Split(':')[1]

        Write-Verbose "Azure AD Application API Permission [$i] => ServicePrincipal: $apiServicePrincipalName"
        Write-Verbose "Azure AD Application API Permission [$i] => Permission: $apiServicePrincipalName"

        $apiServicePrincipal = Get-AzureADServicePrincipal -All $true | Where-Object { $_.DisplayName -eq $apiServicePrincipalName }
        $apiPermission = $apiServicePrincipal.AppRoles | Where-Object { $_.Value -eq $apiPermissionName }

        $apiResourceAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new()
        $apiResourceAccess.ResourceAppId = $apiServicePrincipal.AppId
        $apiResourceAccess.ResourceAccess = [Microsoft.Open.AzureAD.Model.ResourceAccess]::new($apiPermission.Id, 'Role')

        $apiResourceAccessAll = @(Get-AzureADApplication -ObjectId $application.ObjectId | Select-Object -ExpandProperty 'RequiredResourceAccess')
        $apiResourceAccessExist = $false
        if ($null -ne $apiResourceAccessAll -and $apiResourceAccessAll.Count -gt 0)
        {
            $apiResourceAccessExist = [System.Boolean] ($apiResourceAccessAll | Where-Object { $_.ResourceAppId -eq $apiResourceAccess.ResourceAppId -and $_.ResourceAccess.Id -eq $apiResourceAccess.ResourceAccess.Id -and $_.ResourceAccess.Type -eq $apiResourceAccess.ResourceAccess.Type })
        }

        if ($apiResourceAccessExist)
        {
            Write-Verbose "Azure AD Application API Permission [$i] => Use existing Resource Access"
        }
        else
        {
            # Exit if the user does not confirm the application registration.
            if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Add Resource Access')) { return }

            Write-Verbose "Azure AD Application API Permission [$i] => Register new Resource Access"

            # Use an ArrayList instead of an array, becaue the type used object
            # type [Microsoft.Open.AzureAD.Model.RequiredResourceAccess] has a
            # behaviour that adding elements to array (+=) always ends in an
            # error: Does not contain a method named 'op_Addition'.
            $apiResourceAccessNew = [System.Collections.ArrayList]::new()
            $apiResourceAccessAll | ForEach-Object { $apiResourceAccessNew.Add($_) | Out-Null }
            $apiResourceAccessNew.Add($apiResourceAccess) | Out-Null

            Set-AzureADApplication -ObjectId $application.ObjectId -RequiredResourceAccess $apiResourceAccessNew
        }

        # Grant-MicrosoftOnlineAdminConsent -TenantId $context.TenantId -ClientId $application.AppId -ClientId2 $application.ObjectId -ClientSecret (Protect-String -String $secretKey.Value) -ResourceId $principal.ObjectId -Scope $apiPermissionName
    }


    ## AzureAD Fallback User

    # First, check if we have a username for the fallback user. If not
    # specified, generate the default user id.
    $fallbackUsername = 'powershell-automation@{0}' -f $context.TenantDomain
    $fallbackPassword = $null
    if ($null -ne $FallbackCredential)
    {
        $fallbackUsername = $FallbackCredential.UserName
        $fallbackPassword = $FallbackCredential.GetNetworkCredential().Password
    }

    # Now, we check if the use exists on the tenant.
    $fallbackAccount = Get-AzureADUser -Filter "userPrincipalName eq '$fallbackUsername'"
    if ($null -eq $fallbackAccount)
    {
        Write-Verbose 'Azure AD Fallback User => Existing: False'
        Write-Verbose "Azure AD Fallback User => Username: $fallbackUsername"

        if ($null -eq $fallbackPassword)
        {
            Write-Verbose 'Azure AD Fallback User => Password: *** (generated)'

            $fallbackPassword = New-MicrosoftOnlinePassword
        }
        else
        {
            Write-Verbose "Azure AD Fallback User => Password: *** (specified)"
        }

        # Exit if the user does not confirm the application registration.
        if (-not $PSCmdlet.ShouldProcess($fallbackUsername, 'Create Fallback User')) { return }

        $fallbackPasswordProfile = [Microsoft.Open.AzureAD.Model.PasswordProfile]::new()
        $fallbackPasswordProfile.ForceChangePasswordNextLogin = $false
        $fallbackPasswordProfile.EnforceChangePasswordPolicy = $false
        $fallbackPasswordProfile.Password = $fallbackPassword

        $fallbackNewUserSplat = @{
            UserPrincipalName = $fallbackUsername
            DisplayName       = $fallbackUsername.Split('@')[0]
            MailNickName      = $fallbackUsername.Split('@')[0]
            AccountEnabled    = $true
            PasswordProfile   = $fallbackPasswordProfile
        }
        $fallbackAccount = New-AzureADUser @fallbackNewUserSplat
    }
    else
    {
        Write-Verbose "Azure AD Fallback User => Existing: True"
        Write-Verbose "Azure AD Fallback User => Username: $fallbackUsername"

        if ($null -eq $fallbackPassword)
        {
            do
            {
                Write-Host "`nChoose`nHow to process with the password of the existing fallback user?`n[E] Enter the current password [R] Reset with a random password: " -NoNewline
                $fallbackPasswordOption = Read-Host
            }
            while ($fallbackPasswordOption -notin 'E', 'R')

            if ($fallbackPasswordOption -eq 'E')
            {
                $fallbackPassword = Read-Host -Prompt 'Fallback Password' -AsSecureString | Unprotect-SecureString
            }

            if ($fallbackPasswordOption -eq 'R')
            {
                $fallbackPassword = New-MicrosoftOnlinePassword
            }
        }
        else
        {
            do
            {
                Write-Host "`nChoose`nHow to process with the password of the existing fallback user?`n[U] Use current password [R] Reset current password: " -NoNewline
                $fallbackPasswordOption = Read-Host
            }
            while ($fallbackPasswordOption -notin 'U', 'R')
        }

        if ($fallbackPasswordOption -eq 'R')
        {
            # Exit if the user does not confirm the application registration.
            if (-not $PSCmdlet.ShouldProcess($fallbackUsername, 'Set Fallback User Password')) { return }

            Set-AzureADUserPassword -ObjectId $fallbackUsername -Password (Protect-String -String $fallbackPassword) -ForceChangePasswordNextLogin $false -EnforceChangePasswordPolicy $false
        }
    }

    Write-Verbose "Azure AD Fallback User => ObjectId: $($fallbackAccount.ObjectId)"



    ## AzureAD Fallback Directory Role Template

    for ($i = 0; $i -lt $FallbackDirectoryRole.Count; $i++)
    {
        $role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq $FallbackDirectoryRole[$i] }

        if ($null -eq $role)
        {
            $roleTemplate = Get-AzureADDirectoryRoleTemplate | Where-Object { $_.DisplayName -eq $FallbackDirectoryRole[$i] }

            if ($null -ne $roleTemplate)
            {
                # Exit if the user does not confirm.
                if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Enable Role Template')) { return }

                Write-Verbose "Azure AD Fallback Role Template [$i] => Enable Role Template"

                Enable-AzureADDirectoryRole -RoleTemplateId $roleTemplate.ObjectId
            }
            else
            {
                Write-Verbose "Azure AD Fallback Role Template [$i] => Role Template missing"
            }
        }
        else
        {
            Write-Verbose "Azure AD Fallback Role Template [$i] => Use existing Role Template"
        }

        Write-Verbose "Azure AD Fallback Role Template [$i] => DisplayName: $($FallbackDirectoryRole[$i])"
    }


    ## AzureAD Fallback Directory Role

    for ($i = 0; $i -lt $FallbackDirectoryRole.Count; $i++)
    {
        $role = Get-AzureADDirectoryRole | Where-Object { $_.DisplayName -eq $FallbackDirectoryRole[$i] }

        if ($null -ne $role)
        {
            $member = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId | Where-Object { $_.ObjectType -eq 'User' -and $_.UserPrincipalName -eq $fallbackUsername }

            if ($null -ne $member)
            {
                Write-Verbose "Azure AD Fallback Role [$i] => Use existing Role"
            }
            else
            {
                # Exit if the user does not confirm.
                if (-not $PSCmdlet.ShouldProcess($context.TenantDomain, 'Add User to Role')) { return }

                Write-Verbose "Azure AD Fallback Role [$i] => Add User to Role"

                $member = Add-AzureADDirectoryRoleMember -ObjectId $role.ObjectId -RefObjectId $fallbackAccount.ObjectId
            }

            Write-Verbose "Azure AD Fallback Role [$i] => DisplayName: $($FallbackDirectoryRole[$i])"
        }
    }


    ## Ask for Consent

    # Start the browser to request the admin consent by using the Internet
    # Explorer in the InPrivate mode.
    $adminConsentUrl = "https://login.microsoftonline.com/$($context.TenantId)/adminconsent?client_id=$($application.AppId)"
    Write-Verbose "Grant admin constent: $adminConsentUrl"
    & 'C:\Program Files\Internet Explorer\iexplore.exe' -private $adminConsentUrl


    ## MicrosoftOnlineFever Tenant

    if (-not $PSBoundParameters.ContainsKey('Name'))
    {
        $Name = $context.TenantDomain.Split('.')[0].ToUpper()
    }

    # Create and return the tenant.
    $tenantSplat = @{
        Name                  = $Name
        TenantId              = $context.TenantId
        TenantDomain          = $context.TenantDomain
        FallbackUsername      = $fallbackUsername
        FallbackPassword      = Protect-String -String $fallbackPassword
        ApplicationId         = $application.AppId
        ClientId              = $application.AppId
        ClientSecret          = Protect-String -String $secretKey.Value
        CertificateThumbprint = $certificate.Thumbprint
    }
    Add-MicrosoftOnlineTenant @tenantSplat
}